敏捷软件开发 – 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、

时间: 2024-08-24 00:08:16

敏捷软件开发 – ABSTRACT SERVER模式、ADAPTER模式和BRIDGE模式的相关文章

敏捷软件开发:原则、模式与实践——第10章 LSP:Liskov替换原则

第10章 LSP:Liskov替换原则    Liskov替换原则:子类型(subtype)必须能够替换掉它们的基类型(base type). 10.1 违反LSP的情形 10.1.1 简单例子 对LSP的违反导致了OCP的违反: struct Point { double x, y;} public enum ShapeType { square, circle }; public class Shape { private ShapeType type; public Shape(Shape

敏捷软件开发(4)--- TEMPLATE METHOD & STRATEGY 模式

1.TEMPLATE METHOD 泛型,也就是这个模式,是可以基于泛型的. 我们往往会有一些算法,比如排序算法.它的算法部分,我可以把它放在一个基类里面,这样具体类型的比较可以放在子类里面. 看如下冒泡排序算法: package com.joyfulmath.agileexample.template.method; /** * @author deman.lu * @version on 2016-06-09 10:04 */ public abstract class BubbleSort

敏捷软件开发:原则、模式与实践——第19章 类图

第19章 类图 19.1 基础知识19.1.1 类 类一般表示成下面的样子: 分成格间的类图标以及对应的代码 注意类图标中变量和函数名前面的符合.(-)表示private:(#)表示protected:(+)表示public. 19.1.2 关联 类之间的关联表示的是那些持有对其他对象引用的实例变量.如phone和Button之间的关联: 一个PhoneBook对象和多个PhoneNumber对象相连(星号表示许多): 19.1.3 继承 UML中所有的箭头都指向源代码依赖的方向.类和类之间的继

敏捷软件开发:原则、模式与实践——第12章 ISP:接口隔离原则

第12章 ISP:接口隔离原则 不应该强迫客户程序依赖并未使用的方法. 这个原则用来处理“胖”接口所存在的缺点.如果类的接口不是内敛的,就表示该类具有“胖”接口.换句话说,类的“胖”接口可以分解成多组方法.每一组方法都服务于一组不同的客户程序.这样,一些客户程序可以使用一组成员函数,而其他客户程序可以使用其他组的成员函数. ISP承认一些对象确实需要非内敛的接口,但是ISP建议客户不应该看到它们作为单一的类存在.相反,客户程序看到的应该是多个具有内敛接口的抽象基类. 12.1 接口污染 如果子类

敏捷软件开发:原则、模式与实践——第11章 DIP:依赖倒置原则

第11章 DIP:依赖倒置原则 DIP:依赖倒置原则: a.高层模块不应该依赖于低层模块.二者都应该依赖于抽象. b.抽象不应该依赖于细节.细节应该依赖于抽象. 11.1 层次化 下图展示了一个简单的层次化方案: 高层的Policy层使用了低层的Mechanism层,而Mechanism层又使用了更细节的Utility层.它存在一个隐伏的错误特征,那就是:Policy层对于其下一直到Utility层的改动都是敏感的.依赖关系是传递的. 下图展示了一个更为合适的模型: 每个较高层次都为它所需要的服

敏捷软件开发:原则、模式与实践——第5章 重构

第5章 重构 在Martin Fowler的名著<重构>一书中,他把重构定义为:“在不改变代码外在行为的前提下对对代码做出修改,以改进代码内部结构的过程.”可是我们为什么要改进已经能够工作的代码结构呢?我们不是都知道“如果它没有坏,就不要去修理它!”吗? 每一个软件模块都有3项职责.第一个职责是它运行起来所完成的功能.这也是该模块得以存在的原因.第二个职责是它要应对的变化.几乎所有的模块在它们的生命周期中都要变化,开发者有责任保证这种变化应尽可能地简单.一个难以改变的模块是有问题的,即使能够工

敏捷软件开发:原则、模式与实践——第16章 对象图、第17章 用例、第18章 顺序图

第16章 对象图 有时,呈现出系统在某个特定时刻的状态是非常有用的.和一个正在运行系统的快照类似.UML对象图展示了在一个给定时刻获取到的对象.关系和属性值. 不过,你应该对花太多的对象图保持警惕.在大部分的情况下,它们都可以从相应的类图中直接推导出来,因此没有多少用处. 第17章 用例 在所有的UML图中,用例图是最令人迷惑也是最没有用处的.我建议出来系统边界外,忽略掉所有其他的图.系统边界图示例如下: 大矩形是系统边界.矩形内的所有东西都是将要开发的系统的组成部分.矩形外面是操作系统的参与者

敏捷软件开发:原则、模式与实践——第15章 状态图

第15章 状态图 在描述有限状态机(FSM)方面,UML提供个丰富的符合. 15.1 基础知识 下图是一个简单的状态迁移图(STD),该图描述了控制用户登录到系统的FSM.圆角矩形表状态.上层格间放置每个状态的名字.下层格间中放置的是一些特定动作,表示当进入或退出该状态时要做什么. 图中左上角的实心圆称为初始伪状态.FSM从这个伪状态开始,根据变迁规则进行转移. 15.1.1 特定事件 状态图的下层格间含有事件/动作对. 15.1.2 超状态 当许多状态以同样的方式响应某些同样的事件时,使用超状

敏捷软件开发:原则、模式与实践——第20章 咖啡的启示

第20章 咖啡的启示 这个例子对于教学有很多好处.它短小.易于理解并且展示了如何应用面向对象设计原则去管理依赖和分类关注点.但从另一方面来说,它的短小也意味着这种分离带来的好处可能抵不过其成本.就当做一个设计思路来看吧. 20.1 Mark IV型专用咖啡机20.1.1 规格说明书 Mark IV型专用咖啡机一次可以产出12杯咖啡.使用者把过滤器放置在支架上,在其中装入研磨好的咖啡,然后把支架推入其容器中.接着,使用者向滤水器中倒入12杯水并按下冲煮(Brew)按钮.水一直加热到沸腾.不断产生的