1. 程式人生 > >《Head.First設計模式》的學習筆記(15)--代理模式

《Head.First設計模式》的學習筆記(15)--代理模式

 
意圖:

為另一個物件提供一個替身或佔位符得以訪問這個物件。

結構:

接著我們來看RMI遠端代理:


1.我們先在伺服器註冊好幾個糖果機,由於我們現在使用RMI,我們需要構造糖果機和狀態。

糖果機首先變成一個服務,我們為糖果機建立一個遠端介面,讓開介面提供了一組可以遠端呼叫的的方法。

public interface GumballMachineRemote extends Remote {
	public int getCount() throws RemoteException;
	public String getLocation() throws RemoteException;
	public State getState() throws RemoteException;
}

 接著我們繼承這個介面的糖果機

import java.rmi.*;
import java.rmi.server.*;
 
public class GumballMachine
		extends UnicastRemoteObject implements GumballMachineRemote 
{
	State soldOutState;
	State noQuarterState;
	State hasQuarterState;
	State soldState;
	State winnerState;
 
	State state = soldOutState;
	int count = 0;
 	String location;

	public GumballMachine(String location, int numberGumballs) throws RemoteException {
		soldOutState = new SoldOutState(this);
		noQuarterState = new NoQuarterState(this);
		hasQuarterState = new HasQuarterState(this);
		soldState = new SoldState(this);
		winnerState = new WinnerState(this);

		this.count = numberGumballs;
 		if (numberGumballs > 0) {
			state = noQuarterState;
		} 
		this.location = location;
	}
 
 
	public void insertQuarter() {
		state.insertQuarter();
	}
 
	public void ejectQuarter() {
		state.ejectQuarter();
	}
 
	public void turnCrank() {
		state.turnCrank();
		state.dispense();
	}

	void setState(State state) {
		this.state = state;
	}
 
	void releaseBall() {
		System.out.println("A gumball comes rolling out the slot...");
		if (count != 0) {
			count = count - 1;
		}
	}

	public void refill(int count) {
		this.count = count;
		state = noQuarterState;
	}
 
	public int getCount() {
		return count;
	}
 
    public State getState() {
        return state;
    }
 
    public String getLocation() {
        return location;
    }
  
    public State getSoldOutState() {
        return soldOutState;
    }

    public State getNoQuarterState() {
        return noQuarterState;
    }

    public State getHasQuarterState() {
        return hasQuarterState;
    }

    public State getSoldState() {
        return soldState;
    }

    public State getWinnerState() {
        return winnerState;
    }
 
	public String toString() {
		StringBuffer result = new StringBuffer();
		result.append("\nMighty Gumball, Inc.");
		result.append("\nJava-enabled Standing Gumball Model #2004");
		result.append("\nInventory: " + count + " gumball");
		if (count != 1) {
			result.append("s");
		}
		result.append("\n");
		result.append("Machine is " + state + "\n");
		return result.toString();
	}
}

其中State要進行傳送,所以我們要將其序列化:

import java.io.*;
  
public interface State extends Serializable {
	public void insertQuarter();
	public void ejectQuarter();
	public void turnCrank();
	public void dispense();
}

由於狀態有糖果機的引用我們不希望糖果機被傳送,我們只要將其非序列化,如下:

public class NoQuarterState implements State {
    transient GumballMachine gumballMachine;
 
    public NoQuarterState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }
 
	public void insertQuarter() {
		System.out.println("You inserted a quarter");
		gumballMachine.setState(gumballMachine.getHasQuarterState());
	}
 
	public void ejectQuarter() {
		System.out.println("You haven't inserted a quarter");
	}
 
	public void turnCrank() {
		System.out.println("You turned, but there's no quarter");
	 }
 
	public void dispense() {
		System.out.println("You need to pay first");
	} 
 
	public String toString() {
		return "waiting for quarter";
	}
}

加上transient,就可以避免序列化。

最後我們將其註冊到RMI registry中:

package headfirst.proxy.gumball;
import java.rmi.*;

public class GumballMachineTestDrive {
 
	public static void main(String[] args) {
		GumballMachineRemote gumballMachine = null;
		int count;

		if (args.length < 2) {
			System.out.println("GumballMachine <name> <inventory>");
 			System.exit(1);
		}

		try {
			count = Integer.parseInt(args[1]);

			gumballMachine = 
				new GumballMachine(args[0], count);
			Naming.rebind("//" + args[0] + "/gumballmachine", gumballMachine);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

接著我們來看客戶端,也就是監控機:

import java.rmi.*;
 
public class GumballMonitor {
	GumballMachineRemote machine;
 
	public GumballMonitor(GumballMachineRemote machine) {
		this.machine = machine;
	}
 
	public void report() {
		try {
			System.out.println("Gumball Machine: " + machine.getLocation());
			System.out.println("Current inventory: " + machine.getCount() + " gumballs");
			System.out.println("Current state: " + machine.getState());
		} catch (RemoteException e) {
			e.printStackTrace();
		}
	}
}

接著我們在呼叫客戶端的時候實現代理,然後用代理呼叫遠端服務端的方法,程式碼如下:

import java.rmi.*;
 
public class GumballMonitorTestDrive {
 
	public static void main(String[] args) {
		
		//這些是代表服務端啟用了3個這樣地址的RMI服務端
		String[] location = {"rmi://santafe.mightygumball.com/gumballmachine",
		                     "rmi://boulder.mightygumball.com/gumballmachine",
		                     "rmi://seattle.mightygumball.com/gumballmachine"}; 
		
		GumballMonitor[] monitor = new GumballMonitor[location.length];
 
		for (int i=0;i < location.length; i++) {
			try {
				//這個是指建立遠端代理,而且只是呼叫介面避免將真正的糖果機裸露在外
				//由於是迴圈,為每個地址建立了一個代理
           		GumballMachineRemote machine = 
						(GumballMachineRemote) Naming.lookup(location[i]);
           		monitor[i] = new GumballMonitor(machine);
				System.out.println(monitor[i]);
        	} catch (Exception e) {
            	e.printStackTrace();
        	}
		}
 
		for(int i=0; i < monitor.length; i++) {
			//report方法呼叫了代理的遠端方法,相當於呼叫服務端的方法
			monitor[i].report();
		}
	}
}

上述就是一個遠端代理的,遠端代理是可以作為另一個JVM上物件的本地代表。



 

接著我們將介紹虛擬代理,虛擬代理作為建立開銷大的物件的代表。虛擬物件經常直到我們真正需要一個物件的時候才建立它。當物件在建立前和建立中,有虛擬物件來扮演物件的替身,物件建立後,代理就會將請求直接委託給物件。

例子:

我們經常會碰到JFrame載入一個大的網路圖片,這時候我們就可以先使用代理顯示正在載入圖片,等圖片真正載入好我們才“畫”上這個圖片,先看類圖:



 
 

Icon是使用Swing的Icon介面,在使用者介面上顯示圖片。

接著我們來實現ImageIcon繼承了Icon介面

package headfirst.proxy.virtualproxy;

import java.net.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

class ImageProxy implements Icon {
	ImageIcon imageIcon;
	URL imageURL;
	Thread retrievalThread;
	boolean retrieving = false;
     
	public ImageProxy(URL url) { imageURL = url; }
     
	public int getIconWidth() {
		if (imageIcon != null) {
            return imageIcon.getIconWidth();
        } else {
			return 800;
		}
	}
 
	public int getIconHeight() {
		if (imageIcon != null) {
            return imageIcon.getIconHeight();
        } else {
			return 600;
		}
	}
     
	public void paintIcon(final Component c, Graphics  g, int x,  int y) {
		if (imageIcon != null) {
			imageIcon.paintIcon(c, g, x, y);
		} else {
			g.drawString("Loading CD cover, please wait...", x+300, y+190);
			if (!retrieving) {
				retrieving = true;

				retrievalThread = new Thread(new Runnable() {
					public void run() {
						try {
							imageIcon = new ImageIcon(imageURL, "CD Cover");
							c.repaint();
						} catch (Exception e) {
							e.printStackTrace();
						}
					}
				});
				retrievalThread.start();
			}
		}
	}
}

我們先接著看

import java.awt.*;
import javax.swing.*;

class ImageComponent extends JComponent {
	private Icon icon;

	public ImageComponent(Icon icon) {
		this.icon = icon;
	}

	public void setIcon(Icon icon) {
		this.icon = icon;
	}

	public void paintComponent(Graphics g) {
		super.paintComponent(g);
		int w = icon.getIconWidth();
		int h = icon.getIconHeight();
		int x = (800 - w)/2;
		int y = (600 - h)/2;
		icon.paintIcon(this, g, x, y);
	}
}

最後我們來看主程式其中呼叫的程式碼

	 Icon icon = new ImageProxy(initialURL);
  imageComponent = new ImageComponent(icon);
  frame.getContentPane().add(imageComponent);
  frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  frame.setSize(800,600);
  frame.setVisible(true);

現在我們來看看這虛擬代理到底是如何執行的。

1.我們先初始化了一個代理,只是將遠端url串構造到代理中。

2.然後我們將icon這個代理委託到ImageComponent中。

3.到上述為止,我們依然沒有看到任何代理的影子,接著就是見證神奇的一面。接著我們呼叫新增到JFrame視窗,由於呼叫這個方法,會先呼叫ImageComponent中的paintComponent,接著我們依次呼叫 icon.getIconWidth()和icon.getIconHeight(),最重要我們呼叫代理中的icon.paintIcon(this, g, x, y);
在上述方法呼叫過程中個,代理中的ImageIcon物件均為null,這個if-else就很明顯,這邊在詳解一下paintIcon()方法,由於第一次呼叫imageIcon為空,然後我們建立一個執行緒類,然後將這個執行緒類啟動,這個執行緒類到底做了什麼呢?看下面程式碼:

	imageIcon = new ImageIcon(imageURL, "CD Cover");
	c.repaint();

 這邊等待imageIcon載入成功後,重新重新整理ImageCompontent方法。最後顯示圖片。

最後我們將介紹一個代理,利用JAVA-API實現的一個動態代理,這邊我們將實現保護代理。類圖與普通的代理有點不同



 代理變成兩個類。然後讓我們看一下具體的例子,我們希望去保護一個人的具體資訊,這些資訊只有本人能夠進行修改,而評價只有非本人進行修改。

首先我們先實現一個介面:

public interface PersonBean {
 
	String getName();
	String getGender();
	String getInterests();
	int getHotOrNotRating();
 
    void setName(String name);
    void setGender(String gender);
    void setInterests(String interests);
    void setHotOrNotRating(int rating); 
 
}

接著我們要實現兩個InvocationHandler,一個是擁有者,另一個是非擁有者的:

package headfirst.proxy.javaproxy;
 
import java.lang.reflect.*;
 
public class OwnerInvocationHandler implements InvocationHandler { 
	PersonBean person;
 
	public OwnerInvocationHandler(PersonBean person) {
		this.person = person;
	}
 
	public Object invoke(Object proxy, Method method, Object[] args) 
			throws IllegalAccessException {
  
		try {
			if (method.getName().startsWith("get")) {
				return method.invoke(person, args);
   			} else if (method.getName().equals("setHotOrNotRating")) {
				throw new IllegalAccessException();
			} else if (method.getName().startsWith("set")) {
				return method.invoke(person, args);
			} 
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } 
		return null;
	}
}
package headfirst.proxy.javaproxy;
 
import java.lang.reflect.*;
 
public class NonOwnerInvocationHandler implements InvocationHandler { 
	PersonBean person;
 
	public NonOwnerInvocationHandler(PersonBean person) {
		this.person = person;
	}
 
	public Object invoke(Object proxy, Method method, Object[] args) 
			throws IllegalAccessException {
  
		try {
			if (method.getName().startsWith("get")) {
				return method.invoke(person, args);
   			} else if (method.getName().equals("setHotOrNotRating")) {
				return method.invoke(person, args);
			} else if (method.getName().startsWith("set")) {
				throw new IllegalAccessException();
			} 
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } 
		return null;
	}
}

接著我們將看到我們如何建立兩個代理

package headfirst.proxy.javaproxy;

import java.lang.reflect.*;
import java.util.*;

public class MatchMakingTestDrive {
	Hashtable datingDB = new Hashtable();
 	
	public static void main(String[] args) {
		MatchMakingTestDrive test = new MatchMakingTestDrive();
		test.drive();
	}
 
	public MatchMakingTestDrive() {
		initializeDatabase();
	}

	public void drive() {
		//從資料庫中取出一個數據
		PersonBean joe = getPersonFromDatabase("Joe Javabean"); 
	    //建立一個擁有者物件
		PersonBean ownerProxy = getOwnerProxy(joe);
		//呼叫getter方法
		System.out.println("Name is " + ownerProxy.getName());
		//呼叫setter方法
		ownerProxy.setInterests("bowling, Go");
		System.out.println("Interests set from owner proxy");
		try {
			//試著呼叫評價方法,會丟擲異常
			ownerProxy.setHotOrNotRating(10);
		} catch (Exception e) {
			System.out.println("Can't set rating from owner proxy");
		}
		System.out.println("Rating is " + ownerProxy.getHotOrNotRating());
		//建立一個非擁有者物件
		PersonBean nonOwnerProxy = getNonOwnerProxy(joe);
		//
		System.out.println("Name is " + nonOwnerProxy.getName());
		try {
			//呼叫setter方法,會丟擲異常
			nonOwnerProxy.setInterests("bowling, Go");
		} catch (Exception e) {
			System.out.println("Can't set interests from non owner proxy");
		}
		//呼叫評價方法,沒有問題
		nonOwnerProxy.setHotOrNotRating(3);
		System.out.println("Rating set from non owner proxy");
		System.out.println("Rating is " + nonOwnerProxy.getHotOrNotRating());
	}

	PersonBean getOwnerProxy(PersonBean person) {
 		
        return (PersonBean) Proxy.newProxyInstance( 
            	person.getClass().getClassLoader(),
            	person.getClass().getInterfaces(),
                new OwnerInvocationHandler(person));
	}

	PersonBean getNonOwnerProxy(PersonBean person) {
		
        return (PersonBean) Proxy.newProxyInstance(
            	person.getClass().getClassLoader(),
            	person.getClass().getInterfaces(),
                new NonOwnerInvocationHandler(person));
	}

	PersonBean getPersonFromDatabase(String name) {
		return (PersonBean)datingDB.get(name);
	}

	void initializeDatabase() {
		PersonBean joe = new PersonBeanImpl();
		joe.setName("Joe Javabean");
		joe.setInterests("cars, computers, music");
		joe.setHotOrNotRating(7);
		datingDB.put(joe.getName(), joe);

		PersonBean kelly = new PersonBeanImpl();
		kelly.setName("Kelly Klosure");
		kelly.setInterests("ebay, movies, music");
		kelly.setHotOrNotRating(6);
		datingDB.put(kelly.getName(), kelly);
	}
}

總結:

1.代理模式還有很多變種,例如快取代理,同步代理,防火牆代理,和寫入時複製代理

2.代理在結構上類似裝飾者,但是目的不同哦,裝飾者為物件加上行為,而代理是控制行為。

3.代理模式是要實現介面,而介面卡是要改變介面的實現。

4.代理模式也會造成設計中類的數目增加。