代理模式
代理是一个对象,可以用来控制对另一个对象的访问。与另外那个对象实现了同样的接口,并且会把任何方法调用传递给那个对象。另外那个对象成为本体。代理可以代替其本体本实例化,并使其可被远程访问。它还可以把本体的实例化推迟到真正需要的时候。1、代理的结构:
代理最基本的形式是对访问进行控制。代理对象和另一个对象实现的是同样的接口。实际上工作的还是本体,它才是负责执行所分派的任务的那个对象或类。代理对象并不会想装饰者模式那样在另一个对象的基础上添加或修改方法,也不会像外观模式那样简化那个对象的接口。它实现的接口与本体完全相同,所有对它进行的方法调用都会被传递给本体。
1.1、代理如何控制对本体的访问
那种根本不实现任何访问控制的代理最简答,它所做的只是把所有方法调用传递到本体。这种代理毫无用处,但它也可提供一个进一步发展的基础。
示例:创建一个代表图书馆的类 PublicLibrary ,该类封装一个Book对象
一个没有实现任何访问控制的PublicLibrary类的代理: example
这种类型的代理没有什么用处。在各种类型的代理中,虚拟代理是最有用的类型之一。
虚拟代理用于控制对那种创建开销很大的本体的访问。它会把本体的实例化推迟到有方法被调用的时候,有时候还会提供关于实例化状态的反馈。
假设PublicLibrary的实例化很慢,不能在网页加载的时候立即完成。我们可以为其创建一个虚拟代理。让它把PublicLibrary的实例化推迟到必要的时候:
example
PublicLibraryProxy和PublicLibraryVirtualProxy之间的关键区别在于后者不会立即创建PublicLibrary的实例。PublicLibraryVirtualProxy会把构造函数的参数保存起来,知道有方法被调用时才真正执行本体的实例化。
1.2、虚拟代理、远程代理和保护代理
对Javascript程序员来说,虚拟代理可能是最有用的代理类型。
远程代理:用于访问位于另一个环境中的对象。在Java中,这意味着另一个虚拟机中的对象。这种类型的代理很难照搬到Javascript中。首先,Javascript运行时环境不可能长期存在。第二,在Javascript中无法建立到另一个运行时环境的套接字连接以访问其变量空间。与此最接近的一种做法只是用JSON对方法调用进行序列化,然后用Ajax技术将结果发送给某个资源。
远程代理的一种更有可能的用途是控制对其他语言中的本体的访问,这种本体可能是一个Web服务资源,也可能是一个PHP对象。
保护代理也不容易照搬到Javascript中,在Javascript中,你无法判断调用方法的客户类型,因此也就不可能实现这种模式。
出于上述原因,本章集中讨论的是虚拟代理和远程代理。
1.3、代理模式与装饰者模式的比较
代理在许多方法都很想装饰者。装饰者和虚拟代理都要对其他对象进行包装,都要实现与被包装对象相同的接口,而且都要把方法调用传递给包装对象。
区别:
装饰者会对被包装对象的功能进行修改或扩充,而代理只不过是控制对它的访问。代理可能会添加一些控制代码,而并不会对传递给本体的方法调用进行修改。而装饰者就是为修改方法而生的。
装饰者中被包装对象的实例化过程是完全独立的,可以随意裹上一个或多个装饰者。而在代理模式中,被包装对象的实例化是代理的实例化过程的一部分。代理不会像装饰者那样互相包装,它们一次只使用一个。
2、代理模式的适用场合
虚拟代理是一个对象,用于控制对一个创建开销昂贵的资源的访问。虚拟代理是一种优化模式。如果有些类或对象需要使用大量内存保存期数据,而你并不需要在实例化完成之后立即访问这些数据,或者其构造函数需要进行大量计算那就应该使用虚拟代理将设置开销的产生推迟到真正需要使用数据的时候。
远程代理:如果需要访问某种远程资源的话,最好是用一个类或对象来包装它,而不是一遍又一遍的手工设置XMLHttpRequest对象。如果包装独享实现了远程资源的所有方法,那它就是一个远程代理。如果它会在运行期间增添一些方法,那它就是一个装饰者如果它简化了该远程资源的接口,那它就是一个门面。远程代理是一种结构型模式,它提供给了一个访问位于其他环境中的资源的原生Javascript API(native Javascript API)。
总之,如果有些类或对象的创建开销较大,而且不需要在实例化完成后立即访问其数据,那么应该使用虚拟代理。如果你有某种远程资源,并且要为该资源提供的所有功能实现对应的方法,那么应该使用远程代理。
3、示例:网页统计
本例将创建一个远程代理,包装了一个用来提供网页统计数据的Web服务。这个Web服务由一系列URL组成,它们各相当于一个拥有可选参数的方法。它在服务器端用什么语言实现并不重要。数据将以JSON格式返回。下面是这个Web服务实现的5个方法:
http://mydomain.com/stats/getPageviews/
http://mydomain.com/stats/getUniques/
http://mydomain.com/stats/getBrowserShare/
http://mydomain.com/stats/getTopSearchTerms/
http://mydomain.com/stats/getMostVisitedPages/
这几个方法都有用来限制搜集统计数据的时间范围的可选参数(startDate和endDate),对于前面4个方法还可以要求只要特定网页的统计数据。
你希望在整个网站中都显示这些统计数据,但只在用户需要的时候才显示。目前的做法是为每个网页进行手工XHR调用:
example
这段代码使用了单例模式的两种较高级的形式,这样可以创建私用属性和方法,接口所需要的那些方法被定义为公用方法。而助理方法则被定义为私用方法。所有公用方法都调用了fetchData这个辅助方法,前面的手工实现版本中那些重复性的代码都被集中到这个方法中。
本例使用远程代理的好处:实现代码与Web服务的耦合变松散,重复代码大大减少。对待StatsProxy对象与对待别的Javascript对象没什么两样,你可以随意用它进行查询。弊端:掩盖了数据来源,实际上还是要对服务器进行访问,需要耗费一定时间。在设计远程代理时需要注明一下这种性能问题。本例中通过借助回调函数进行异步调用稍加缓解,不会因为要等待调用结果而被阻塞。
4、包装Web服务的通用模式
我们可以从上面的例子中提炼出一个更加通用的Web服务包装模式。由于Javascript的同源性限制,Web服务代理所包装的服务必须部署在使用代理的网页所在的域中。这里使用的不是一个单例,而是一个拥有构造函数的普通类,以便以后进行扩展。
example
5、示例:目录查找
为公司网站的主页添加一个可搜索的员工目录。它应该模仿实际的员工花名册中的页面。由于这个网页的访问量很大,所以这个解决方案必须尽量节约带宽,我们不希望这个小小的特性拖累整个网页。
只为那些需要查看员工资料的用户加载这种数据(要知道其数据量相当大)。这是虚拟代理可以大显身手的地方,因为它能够把需要占用大量带宽的资源的加载推迟到必要的时候。同时在加载员工目录的过程中提供一些提示信息。example
要设计一个更复杂的版本,那个初始化检查还可以设计得再健壮一些,实例化过程的触发器也可以设计的更精巧一些。
6、创建虚拟代理的通用模式
Javascript是一种非常灵活的语言。得益于此,你可以创建一个动态虚拟代理,它会检查提供给他的类的接口,创建自己的对应方法,并且将该类的实例化推迟到某些预定条件得到满足的时候。
这个动态代理会把本体的实例化推迟到你认为必要的时候。在实例化完成之前,代理的所有公用方法什么事都不会做。这个类可以用来包装那些需要大量计算或较长时间才能实例化的类。example
7、代理模式之利
远程代理:可以把远程资源当做本地Javascript对象使用,其益处显而易见。减少了为访问远程资源而不得编写的粘合性代码的数量并且为此提供了单一接口。
虚拟代理:有着截然不同的作用,并不会减少重复性的代码和提高对象的模块性,这与本书所讲的大多数模式都不一样。实际上还会在网页中增加一些代码,而这些代码并非必不可少。在速度比较重要的网页中,虚拟代理可以用来把大对象的实例化推迟到其他元素加载完毕之后。虚拟代理的主要好处就在于:你可以用它代替其本体,而不用操心实例化开销的问题。
8、代理模式之弊
不同的代理具有不同的好处,但它们的弊端却是想通的。代理可以掩盖了大量复杂行为。远程代理请求方法的时间比访问本地资源多出几个数量级。此外,远程代理只有在能够与远程资源通信的条件下才能工作。
对于虚拟代理来说,它掩盖了推迟本体的实例化逻辑,使用这种代理的程序员并不清楚有哪些从左会出发对象的实例化。
代理任何时候都可以被替换为本体,它会增加项目的复杂性。除非它能降低你的代码的冗余程度、提高其模块化程度或运行效率,否则不要使用它。如果运用得当,那么代理能够大大简化对资源的访问,这是其他方法难以办到的。