初识设计模式 chapter 11-代理模式
1 引言
终于到了压轴大戏,请屏住呼吸,睁大眼睛,让我们一起来认识本书的最后一个单设计模式:代理模式。
玩过扮白脸、扮黑脸的游戏吗?你是一个白脸,提供很好且很友善的服务,但是你不希望每个人都叫你做事,所以找了黑脸控制对你的访问。这就是代理要做的:控制和管理访问。就像你将看到的,代理的方式有许多种。代理以通过Internet为它们的代理对象搬运的整个方法调用而出名,它也可以代替某些懒惰的对象做一些事情。
2 正文
2.1 本章业务背景
还记得上一章的糖果机吗?现在糖果机CEO要求能实时打印所有糖果机的库存以及机器状态报告。听起来好像很容易,如果你还记得我们已经得到了可以取得糖果数量的getCount()方法和取得糖果机状态的getState()方法。
我们所需要做的事,就是创建一份能打印出来的报告,然后把它传送给CEO。这个嘛!我们可能需要为每个糖果机加上一个位置的字段,这样CEO就可以一目了然了。
2.2 初步设计
我们先为GumballMachine机上处理位置的支持:
public class GumballMachine {
State soldOutState;
State noQuarterState;
State hasQuarterState;
State soldState;
State winnerState;
State state = soldOutState;
int count = 0;
String location;
//位置被传入构造器内,然后存到此实例变量中
public GumballMachine(String location, int count) {
soldOutState = new SoldOutState(this);
noQuarterState = new NoQuarterState(this);
hasQuarterState = new HasQuarterState(this);
soldState = new SoldState(this);
winnerState = new WinnerState(this);
this.count = count;
if (count > 0) {
state = noQuarterState;
}
this.location = location;
}
}
现在让我们创建另一个类,GumballMonitor(糖果监视器),以便取得机器的位置、糖果的库存量以及当前机器的状态,并打印成一份可爱的报告。
public class GumballMonitor {
/*
* 1 此监视器的构造器需要被传入糖果机,它会将糖果机记录在machine实例变量中
* 2 负责打印报告的report方法,会将位置、库存、机器状态打印出来
*/
GumballMachine machine;
public GumballMonitor(GumballMachine machine) {
this.machine = machine;
}
public void report() {
System.out.println("Gumball Machine: " + machine.getLocation());
System.out.println("Current inventory: " + machine.getCount() + " gumballs");
System.out.println("Current state: " + machine.getState());
}
}
2.3 需求变更
CEO要求远程监控糖果机,意思就是CEO可以在自己办公室的电脑上实时看到所有糖果机的状态。
我们来想一想:我们 已经写好监视器的代码,对吧?我们给GumballMonitor一个糖果机的引用,它给我们一份报告。问题在于监视器和糖果机在同一个JVM上面执行,但是CEO希望在他的桌面上远程监控这些机器!所以我们可以不要变化GumballMachine,不要将糖果机交给GumballMonitor,而是将一个远程对象的代理交给他。所谓的代理(proxy),就是代表某个真实的对象。在这个案例中,代理就像是糖果机对象一样,但其实幕后是它利用网络和一个远程的真正糖果机沟通。
我们必须确定糖果机能够通过网络接收请求并且提供服务;我们也需要让监视器有办法取得代理对象的引用,这方面,幸好Java已经有一些很棒的内置工具可以帮助我们。
远程代理就好比“远程对象的本地代表。何谓”远程对象“?这是一种对象,活在不同的Java虚拟机中(更一般的说法是,在不同的地址空间运行的远程对象)。何谓”本地代表“?这是一种可以由本地方法调用的对象,其行为会转发到远程对象中。
你的客户对象所做的就像是在做远程方法调用,但其实只是调用本地堆中的”代理“对象上的方法,再由代理处理所有网络通信的低层细节。
2.4 将远程代理加到糖果机的监视代码中
我们打算这么做:
1、首先,我们先浏览并了解一下RMI。
2、接着,我们会把GumballMachine编程远程服务,提供一些可以被远程调用的方法。
3、然后,我们将创建一个能和远程的GumballMachine沟通的代理,这需要用到RMI。最后再结合监视系统,CEO就可以监视任何数量的远程糖果机了。
Java RMI概况
现在你已经知道远程方法如何工作的要点,你还需要了解如何利用RMI进行远程方法弟阿勇。
RMI提供了客户辅助对象和服务辅助对象,为客户辅助对象创建和服务对象相同的方法。RMI的好处在于你不必亲自写任何网络或I/O代码。客户程序调用远程方法(即真正的服务所在)就和在运行在客户自己的本地JVM上对对象进行正常方法调用一样。
RMI也提供了所有运行时的基础设施,好让这一切正常工作。这包括了查找服务(lookup service),这个服务用来寻找和访问远程对象。
关于RMI调用和本地(正常的)的方法调用,有一个不同点。虽然调用远程方法就如同调用本地方法一样,但是客户辅助对象会通过网络发送方法调用,所以网络和I/O是的确存在的。关于网络和I/O部分,我们知道些什么呢?
我们知道网络和I/O是有风险的,容易失败的,所以随时都可能抛出异常,也因此,客户必须意识到风险的存在。后面我们马上就会讨论这部分。
RMI称呼(译注:terminology,术语,重点在概念本身;nomenclature,称呼,重点概念上贴的标签):RMI客户辅助对象称为stub“桩”,服务辅助对象称为skeleton(骨架).
现在,我们就来看看如何将对象变成服务——可以接受远程调用的服务。也看看,如何让客户做远程调用。
2.5 制作远程服务
这里有用来制作远程服务的五个步骤的概要。
步骤1:制作远程接口。远程接口定义出可以让客户远程调用的方法。客户将用它作为服务的类类型。Stub和实际的服务都实现此接口。
步骤2:制作远程的实现。这是做实际工作的类,为远程接口中定义的远程方法提供了真正的实现。这就是客户真正想要调用的方法的对象(例如,我们的GumballMachine)。
步骤3:利用rmic产生的stub和skeleton。这就是客户和服务的辅助类。你不需要自己创建这些类,甚至连生辰它们的代码都不用看,因为当你运行rmic工具时,这都会自动处理。你可以在JDK中找到rmic。
步骤4:启动RMI Registry(rmiregistry)。rmiregistry就像是电话簿,客户可以从中查到代理的位置(也就是客户的stub helper对象)。
步骤5:开始远程服务。你必须让服务对象开始运行。你的服务实现类会去实例化一个服务的实例,并将这个服务注册到RMI registry。注册之后,这个服务就可以供客户调用了。
注意:对于RMI,程序员最常犯的三个错误
1、忘了在启动远程服务之前先启动rmiregistry(要用Naming.rebind()注册服务,rmiregitry必须是运行的)。
2、忘了让变量和返回值的类型成为可序列化的类型(这种错误无法在编译期返现,只会在运行时发现)。
3、忘了给客户端提供stub类。
2.6 远程糖果监视器
要让我们的代码改成使用代理,第一个步骤是让GumballMachine变成可以接受远程调用。换句话说,我们要把它变成一个服务。做法如下:
1、为GumballMachine创建一个远程接口。该接口提供了一组可以远程调用的方法。
2、确定接口的所有返回类型都是可序列化的。
3、在一个具体类中,实现此接口。
我们从远程接口开始:
public interface GumballMachineRemote extends Remote {
/*
* 1 所有的返回类型都必须是原语类型或可序列化类型
* 2 这是准备支持的方法,每个都要抛出RemoteException
*/
public int getCount() throws RemoteException;
public String getLocation() throws RemoteException;
public State getState() throws RemoteException;
}
我们有一个返回类型不是可序列化的:State类,现在来修改一下
public interface State extends Serializable {
/*
* 1 然后我们只要扩展SerializableJiekou (此接口没有方法)。现在所有子类中的State就可以在网络上传播了。
*/
public void insertQuarter();
public void ejectQuarter();
public void turnCrank();
public void dispense();
}
实际上,我们还没处理完Serializable。对于State,我们有一个问题。你可以记得,每个状态对象都维护着一个队糖果机的引用,这样一来,状态对象就可以调用糖果机的方法,改变糖果机的状态。我们不希望整个糖果机都被序列化并随着State对象一起传送。
修正这点很容易,对于State的每个实现,我们都在GumballMachine实例变量前面加上transient关键字,这样就告诉JVM不要序列化这个字段。
我们已经实现了GumballMachine类,但是需要确定它可以当成服务使用,并处理来自网络上的请求。为了做到这一点,我们必须确定GumballMachine实现GumballMachineRemote接口。
糖果及服务已经完成了。现在我们要将它装上去,好开始接受请求。首先我们要确保将它注册到RMI registry中,好让客户可以找到他。
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);
}
/*
* 1 首先,我们需要再实例化糖果的代码周围加上try/catch块,因为我们的构造器可能抛出异常
* 2 我们也添加上对Naming.rebind的调用,用gumballmachine的名字发布GUmballMachine的stub。
*/
try {
count = Integer.parseInt(args[1]);
gumballMachine =
new GumballMachine(args[0], count);
Naming.rebind("//" + args[0] + "/gumballmachine", gumballMachine);
} catch (Exception e) {
e.printStackTrace();
}
}
}
现在是GumballMachine客户端
public class GumballMonitor {
GumballMachineRemote machine;
//现在我们准备依赖此远程接口,而不是具体的GumballMachine类。
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();
}
}
}
编写监视器测试程序
public class GumballMonitorTestDrive {
public static void main(String[] args) {
/*
* 1 我们创建一个数组,数组内的元素时每台机器的位置
* 2 我们也创建监视器的数组
* 3 现在,需要为每个远程机器创建一个代理
* 4 然后我们遍历每台机器,将报告打印出来
*/
String[] location = {"rmi://santafe.mightygumball.com/gumballmachine",
"rmi://boulder.mightygumball.com/gumballmachine",
"rmi://seattle.mightygumball.com/gumballmachine"};
if (args.length >= 0)
{
location = new String[1];
location[0] = "rmi://" + args[0] + "/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++) {
monitor[i].report();
}
}
}
通过调用代理的方法,远程调用可以跨过网络,返回字符串、整数和State对象。因为我们使用的是代理,调用的方法会在远程执行,GumballMachine根本就不知道/或者不在话这一点(唯一要操心的是:要处理远程调用异常)。
2.7 定义代理模式
代理模式:为另一个对象提供一个替身或者占位符以控制对这个对象的访问。
使用代理模式创建代表(representative)对象,让代表对象控制某对象的访问,被代理的对象可以是远程的对象、创建开销大的对象或需要安全控制的对象。
代理控制访问是怎么解释的?这听起来有一点奇怪。别担心,在糖果机的例子中,代理孔子了对远程对象的访问。代理之所以需要控制访问,是因为我们的客户(监视器)不知道如何和远程对象沟通。从某个方面来看,远程代理控制访问,好帮助我们处理网络上的细节。正如同刚刚说过的,代理模式有许多变体,而这些变体几乎都和“控制访问”的做法有关。稍后我们会对此讨论的更详细,目前哦我们还是先看看几种代理控制访问的方式:
1、就像我们已经知道的,远程代理控制访问的远程对象。
2、虚拟代理控制访问创建开销大的资源。
3、保护代理基于权限控制对资源的访问。
Proxy持有realSubject的引用。在某些例子中,Proxy还会负责RealSubject对象的创建与销毁。客户和RealSubject的交互都必须通过Proxy。因为Proxy和RealSubject实现相同的接口(Subject),所以任何用到RealSubject的地方,都可以用Proxy替代。Proxy也控制了对RealSubject的访问,在某些情况下,我们可能需要这样的额控制。这些情况包括RealSubject是远程的对象、RealSubject创建开销大,或RealSubject需要被保护。
2.8 虚拟代理
你已经看过代理模式的定义,也看过了一个特定的例子(远程代理),现在就让我们看看另一种代理:虚拟代理。你将发现,代理模式可以以很多形式显现,但都大致符合一般代理的设计。为何有这么多形式呢?因为代理模式可以被用在许多不同的例子中。让我们现在看看虚拟代理和远程代理的比较。
远程代理:远程代理可以作为另一个JVM上的对象的本地代理。调用代理的方法,会被代理利用网络转发到远程执行,并且将结果通过网络返回给代理,再由代理将结果转给客户。
虚拟代理:虚拟代理作为创建开销大的对象的代表。虚拟代理经常指导我们真正需要一个对象的时候才创建它。当对象在创建前和创建中时,由虚拟代理来扮演对象的替身。对象创建后,代理就会将请求直接委托给对象。
2.9 保护代理
Java在java.lang.reflect包中有自己的代理支持,利用这个包你可以在运行时,动态的创建一个代理类,实现一个或多个接口,并将方法的调用转发到你所指定的类。因为实际的代理类时在运行时创建,我们称这个Java技术为:动态代理。
我们要利用Java的动态代理创建我们下一个代理实现(保护代理)。但在这之前,先让我们看一下类图,了解一下动态代理是怎么一回事。就和真实世界中大多数的事务一样,它和传统代理模式的传统定义有一点出入。
业务背景:对象村的配对
每个城镇都需要配对服务,不是吗?你负责帮对象村实现约会服务系统。你又一个好点子,就是在服务中加入“Hot”和“Not”的评鉴,“Hot”就表示喜欢对方,“Not”表示不喜欢。你希望这套系统能鼓励你的顾客找到可能的配对对象,这也会让事情更有趣。
你的服务系统涉及到一个PersonBean,允许设置或取得一个人的信息:
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);
}
让我们看看实现
public class PersonBeanImpl implements PersonBean {
String name;
String gender;
String interests;
int rating;
int ratingCount = 0;
public String getName() {
return name;
}
public String getGender() {
return gender;
}
public String getInterests() {
return interests;
}
public int getHotOrNotRating() {
if (ratingCount == 0) return 0;
return (rating/ratingCount);
}
public void setName(String name) {
this.name = name;
}
public void setGender(String gender) {
this.gender = gender;
}
public void setInterests(String interests) {
this.interests = interests;
}
public void setHotOrNotRating(int rating) {
this.rating += rating;
ratingCount++;
}
}
系统不应该允许用户篡改别人的数据,根据我们定义PersonBean的方式,任何客户都可以调用任何方法。
这是一个我们可以使用保护代理的绝佳例子。什么是保护代理?这是一种根据访问权限决定客户可否访问对象的代理。比方说,如果你又一个雇员对象,保护代理允许雇员对象上的某些方法,经理还可以多调用一些其他方法,而人力资源处的雇员可以调用对象上的所有方法。
在我们的约会服务中,我们希望顾客可以设置自己的信息,同时又防止他人更改这些信息。HotOrNot评分则想法,你不能更改自己的评分,但是他人可以设置你的评分。我们再PersonBean中已经有许多getter方法了,每个方法的返回信息都是公开的,任何顾客都可以调用他们。
大局观:为PersonBean创建动态代理
我们有一些问题要修正:顾客不可以改变自己的HotOrNot评分,也不可以改变其他顾客的个人信息。要修正这些问题,你必须创建两个代理:一个访问你自己的PersonBean对象,另一个访问另一顾客的PersonBean对象。这样,代理就可以控制在每一种情况下允许哪一种请求。
创建这种代理,我们必须使用Java API的动态代理,在几页前有这个API的概况。Java会为我们创建两个代理,我们只需要提供handler来处理代理转来的方法。
步骤1:创建两个INvocationHandler。InvocationHandler实现了代理的行为,正如你将看到的,Java负责创建真实代理类和对象。我们只需要提供在方法调用发生时知道做什么的handler。
步骤2:写代码创建动态代理。我们需要写一些代码产生代理类,并实例化它。等一下你就会看到这些代码。
步骤3:利用适当的代理包装任何PersonBean对象。当我们呢需要使用PersonBean对象时,如果不是顾客自己。就是另一个顾客正在检查的服务使用者。不管是哪一种情况,我们都为PersonBean创建适合的代理。
实现OwnerInvocationHandler:
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 {
/*
* 1 如果方法是一个getter,我们就调用Person内的方法
* 2 否则,如果方法是setHotOrNot,我们就抛出异常
* 3 因为我们是拥有者,所以任何其他Set方法都可以调用
*/
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;
}
}
2.10 代理动物园
欢迎来到对象村动物园!现在你知道什么是远程代理、虚拟代理和保护代理了。在野外,你看到的代理还不只这些。在动物园的代理区,我们展示了许多辛苦捕捉来的野生的代理,供你研究。
防火墙代理:控制网络资源的访问,保护主题免于”坏客户“的侵害。
智能引用代理:当主题被引用是,进行额外的动作,例如计算一个对象被引用的次数。
缓存代理:为开销大的运算结果提供暂时存储,它也允许多个客户共享结果,以减少计算或网络延迟。
同步代理:在多线程的情况下为主题提供安全的访问。
复杂隐蔽代理:用来隐藏一个雷的复杂集合的复杂度,并进行控制访问控制。有时候也称为外观代理,这不难理解。复杂隐藏代理和外观模式是不一样的,因为代理控制访问,而外观模式只提供另一组接口。
写入时复制代理:用来控制对象的复制,方法是延迟对象的复制,直到客户真的需要为止。这是虚拟代理的变体。
3 本章小结
最后一个单设计模式,代理模式,终于学完了。庞大的一个设计模式,不仅没有非常明确的类图,而且变种很多,是非常考验实际操作能力的一个模式。按照我的理解,像hook,filter这类广义上也能称之为代理模式,只要不是直接访问远程对象,且代理类可以做访问控制的模式都是代理模式。额,我好想又把文中的概念重复了一遍。后面三章复合设计模式,因为这中间还会穿插Java基础的学习,大概会在月底之前完成所有的笔记。学习的感悟暂且不表,等所有笔记结束后,单独开篇来说说学习心得。