首页 > 代码库 > 敏捷软件开发 – ABSTRACT SERVER模式、ADAPTER模式和BRIDGE模式
敏捷软件开发 – ABSTRACT SERVER模式、ADAPTER模式和BRIDGE模式
设计运行在简易台灯中的软件。台灯由一个开关和一盏灯组成。可以询问开关是开着还是关着,也可以让灯打开或者关闭。
下面设计了一个简易的模型。Switch对象可以轮询实际开关的状态,并且可以发送相应的turnOn和turnOff消息给Light。
这个设计违反了两个设计原则:依赖倒置(DIP)和开放-封闭(OCP)。对DIP的违反是明显的,Switch依赖了具体类Light。DIP告诉我们要优先依赖于抽象类。对OCP的违反虽然没有那么明显,但是更加切中要害。我们之所以不喜欢这个设计是因为它迫使我们在任何需要Switch的地方都要附带上Light。我们不能容易的扩展Switch以管理除Light外的其他对象。
ABSTRACT SERVER模式
在Switch和Light之间引入一个接口,这样就使得Switch能够控制任何实现了这个接口的东西。这立即满足了DIP和OCP。
顺便说一下,请注意接口的名字是从它的客户的角度起的。它称为Switchable而不是Lightable。接口属于它的客户,而不是它的派生类。客户和接口之间的逻辑关系要强于接口和它的派生类之间的逻辑绑定关系。它们之间的关系强到在没有Switchable的情况下就无法使用Switch;但是,再没有Light的情况下完全可以使用Switchable。逻辑关系的强度和实体关系的强度是不一致的。继承是一个比关联强得多的实体关系。
这种逻辑和实体关系强度的不一致性是静态类型语言的产物。动态类型语言不具有这种不一致性,因为它们不用继承的实现多态行为。
ADAPTER模式
上面的设计有一个问题。它可能会违反单一职责原则(SRP)。我们把Light和Switchable绑定在一起,而它们可能会因为不同的原因改变。如果无法把继承关系加到Light上该怎么办?如果从第三方购买了Light,而没有源代码该怎么办?或者如果想让Switch去控制其他一些类,但是却不能让它们从Switchable派生该怎么办呢?
适配器从Switchable派生并委托给Light。现在Switch可以控制任何能够被打开或者关闭的对象。我们所需要做的指示创建一个合适的适配器。事实上,对象甚至不需要具有和Switchable中一样的turnOn和turnOff。适配器会是陪到对象的接口。
使用适配器是有代价的。需要编写新的类,需要实例化适配器并把要失陪的对象和它绑定起来。然后,每当你调用适配器时,必须要付出委托所需的时间和空间代价。所以,你显然不想始终都使用适配器。对大多数情况来说,ABSTRACT SERVER解决方案就非常合适了。
调制解调器问题、适配器以及LSP
假定现在客户提出了一个新的续期。有某些种类的调制解调器是不需要拨号的,它们称为专用调制解调器,但是它们位于一条专用连接的两端。有几个新应用程序使用这些专用调制解调器。它们无需拨号。我们称这些使用者为DedUser。但是,客户喜欢当前所有调制解调器客户程序都可以使用这些专用调制解调器。他们不希望去更改许许多多的调制解调器客户应用程序,所以完全可以让这些调制解调器客户去拨一些假电话号码。
如果能选择的话,我们会把系统设计更改为下图一样。糟糕的是,这样做会要求我们更改所有的调制解调器客户程序,而这是客户不允许的。
杂凑的解决方案
我们可以在DedicateModem的Dial方法和Hangup方法中模拟一个连接状态。如果还没有调用Dial,或者已经调用了Hangup,就可以拒绝返回字符。如果这样做的话,那么所有的调制解调器客户程序都可以正常工作并且也不必更改。只要让DedUser去调用Dial和Hangup即可。
你可能认为这种做法会令那些正在实现DedUser的人觉得非常沮丧。他们明明在使用DedicatedModem。为什么他们还要去调用Dial和Hangup呢?不过,他们的软件还没有开始编写,所以还比较容易让他们按照我们的想法去做。
混乱的依赖关系网
几个月后,已经有了大量的DedUser,此时客户提出一个新的更改。这些年来,我们的程序似乎都没有拨过国际电话号码。这就是为什么在Dial中使用char[10]而没有出问题的原因。但是,现在,客户希望能够拨打任意长度的电话号码。他们需要去拨打国际电话、信用卡电话、PIN标识电话等。
显然,所有的调制解调器客户程序都必须更改。在它们中是用char[10]来表示电话号码的。客户同意了对调制解调器客户程序的更改,因为他们别无选择,我们把大量的程序员投入到这个任务中。同样显然的是,调制解调器层次结构中的类都必须更改以容纳新的电话号码长度。糟糕的是,现在我们必须要去告诉DedUser的编写者,他们也必须要更改他们的代码!你可以想象他们听到这个会有多沮丧。本来他们是不用调用Dial的。他们之所以这么做是因为我们告诉他们必须这样做。现在,他们将要遭受高代价的维护工作,因为他们做了我们让他们做的事情。
这就是许多项目都会具有的那种有害的混乱依赖关系。系统某一部分的一个杂凑体创建了一个有害的依赖关系,最终导致系统中完全无关的部分出现问题。
用ADAPTER模式来解决
如果使用ADAPTER模式解决最初的问题的话,就可以避免这个严重问题。在ADAPTER模式的方案中,DedicatedModem不从Modem继承。调制解调器客户程序通过DedicatedModemAdapter间接的使用DedicatedModem。在这个适配器的Dial和Hangup的实现中去模拟连接状态。它把send和receive调用委托给DedicatedModem。
这消除了我们之前遇到的所有困难。调制解调器的客户程序看到的是它们期望的连接行为,并且DedUser也不比去调用dial和hangup。当改变有关电话号码的需求时,DedUser不会受到影响。因此,通过在适当的位置放至适配器,我们修正了对LSP和OCP的违反。
但是,杂凑体仍然存在。适配器仍然需要模拟连接状态。但是,请注意,所有的依赖关系都是从适配器发起的。杂凑体和系统隔离,藏身于无人知晓的适配器中。只有在某处的某个工厂才可能会实际依赖于这个适配器。
BRIDGE模式
看待上述问题,还有另外一个方式。对于专用调制解调器的需要从Modem类层次结构中增加一个新的自由度。在最初构思Modem类型时,它只是一组不同硬件设备的接口。因此,我们让HayesModem、USRModem和ErniesModem从基类Modem派生。但是,现在出现了另外一种切分Modem层次结构的方式。我们可以让DialModem和DedicateModem从Modem派生。
这不是一个理想的结构。每当增加一款硬件时,就必须创建两个新类:一个针对专用的情况,一个针对拨号的情况。每当增加一种新连接类型时,就必须创建三个新类,分别对应三款不同的硬件。如果这两个自由度根本就是不稳定的,那么用不了多久,就会出现大量的派生类。
我们可以使用BRIDGE模式解决这个问题。在类型层次结构具有多个自由度的情况中,BRIDGE模式通常是有用的。我们可以把这些层次结构分开并通过桥把它们结合在一起,而不是把它们合并起来。
调制解调器的使用者继续使用Modem接口,ModemConnectionController实现了Modem接口。ModemConnectionController的派生类控制着连接机制。DialModemController只是把dial方法和hangup方法传给基类ModemConnectionController中的dialImp和hangImp。接着,这两个方法把调用委托给类ModemImp,在那里它们会被部署到适当的硬件控制器。DedModemController把dial和hangup实现为仿真连接状态。它把send和receive传递给sendImp和receiveImp,并像前面一样再委托给ModemImp层次结构。
这个结构虽然复杂,但是很有趣。它的创建不会影响到调制解调器的使用者,并且还完全分离了连接策略和硬件实现。ModemConnectionController的每个派生类代表了一个新的连接策略。在这个策略的实现中可以使用sendImp、receiveImp、dialImp和hangImp。新imp方法的增加不会影响到使用者。可以使用ISP来给连接控制类增加新的接口。
结论
可能有人非常想说,Modem场景中的真正问题是最初的设计者设计错了。他们本应该知道链接和通讯是不同的概念。如果他们稍稍多做一些分析,就会发现这个问题并且改正它。所以,很容易把问题归结为不充分的分析。
胡说!根本不存在充分分析这种东西。无论花多少时间试图去找出完美的软件结构,客户总是会引入一个变化破坏这个结构。这种情况是无法避免的。不存在完美的结构。只存在那些试图去平衡当前的代价和收益的结构。随着时间的过去,这些结构肯定会随着系统需求的改变而改变。管理这种变化的诀窍是尽可能地保持系统简单、灵活。
摘录自:[美]RobertC.Martin、MicahMartin著,邓辉、孙鸣译 敏捷软件开发原则、模式与实践(C#版修订版) [M]、人民邮电出版社,2013、368-376、
敏捷软件开发 – ABSTRACT SERVER模式、ADAPTER模式和BRIDGE模式