10.6 消除对象耦合
代理(Proxy)模式和状态(State)模式都提供一个代理(Surrogate)类。代码与代理类打交道,而做实际工作的类隐藏在代理类背后。当调用代理类中的一个函数时,代理类仅转而去调用实现类中相应的函数。这两种模式是如此相似,从结构上看,可以认为代理模式只是状态模式的一个特例。设想将这两者合理地混合在一起组成一个称为代理(Surrogate)设计模式,这肯定是一个很具有诱惑力的想法,但是这两个模式的内涵(intent)是不一样的。这样做很容易陷入“如果结构相同模式就相同”的思想误区。必须始终关注模式的内涵,从而明确它的功能到底是什么。
基本思想很简单:代理(Surrogate)类派生自一个基类,由平行地派生自同一个基类的一个或多个类提供实际的实现:
当一个代理对象被创建的时候,一个实现对象就分配给了它,代理对象就将函数调用发给实现对象。
从结构上来看,代理模式和状态模式的区别很简单:代理模式只有一个实现类,而状态模式有多个(一个以上)实现。(在GoF中)认为这两种设计模式的应用也不同:代理模式控制对其实现类的访问,而状态模式动态地改变其实现类。然而,如果广义理解“控制对实现类的访问”,则这两个模式似乎是一个连续体的两部分。
10.6.1 代理模式:作为其他对象的前端
如果按照上面的图结构实现代理模式,其实现代码如下:
在某些情况下,类Implementation并不需要与类Proxy具有相同的接口—Proxy类可以任意“订购”(关联)Implementation类并且将函数调用提交给它,这就符合了代理的基本思想(值得注意的是,这种描述和GoF关于代理的定义不一致)。然而,使用共同的接口可以将代理的替代物插入客户代码中—编写客户代码只用来与原对象进行通信,不需对其进行修改以接受代理(这大概是使用代理的关键问题)。此外,通过共同的接口,Implementation被迫实现Proxy需要调用的所有函数。
代理模式与状态模式之间的不同之处在于它们所解决的问题不同。GoF中给出了代理模式的一般用途,描述如下:
1)远程代理(Remote proxy)。为不同地址空间的对象提供代理。通过某些远程对象技术实现。
2)虚拟代理(Virtual proxy)。根据需要提供一种“惰性初始化”方式来创建高代价的对象。
3)保护代理(Protection proxy)。当不愿意客户程序员拥有被代理对象的全部访问权限时,使用保护代理。
4)巧妙引用(Smart reference)。当访问被代理的对象时,增加额外的活动。引用计数(reference counting)就是一个例子:它用来跟踪被代理的某个特定对象被引用的次数,以实现写入时复制(copy-on-write)并且防止对象起别名。[1]一个更简单的例子就是对特定函数的调用进行计数。
10.6.2 状态模式:改变对象的行为
状态模式产生一个可以改变其类的对象,当发现在大多数或者所有函数中都存在有条件的代码时,这种模式很有用。和代理模式一样,状态模式通过一个前端对象来使用后端实现对象履行其职责。然而,在前端对象生存期期间,状态模式从一个实现对象到另一个实现对象进行切换,以实现对于相同的函数调用产生不同的行为。如果在决定函数该做什么之前在每个函数内部做很多测试,那么这种方法是对实现代码的一种很好的改进。举个例子,在青蛙王子童话中,青蛙王子依照其所处的状态而有不同的行为。现在可以通过测试一个bool变量来实现:
然而,greet()等任何其他所有函数在执行操作前都必须测试变量isFrog,这样就使代码变得笨拙至极,特别是在系统中加入额外的状态时情况会更加严重。通过将操作委派给状态对象,这种情况就可以改变,代码从而得到了简化。
在这里,将实现类设计为嵌套或者私有并不是必需的,但是如果能做到的话,就会创建出更加清晰的代码。
注意,对状态类的改变将会自动地在所有的代码中进行传播,而不需要编辑这些类来完成改变。
[1]参阅《C++编程思想》第1卷以获得关于引用计数更详细的知识。