22.3 观察者模式的应用

22.3.1 观察者模式的优点

  • 观察者和被观察者之间是抽象耦合

如此设计,则不管是增加观察者还是被观察者都非常容易扩展,而且在Java中都已经实现的抽象层级的定义,在系统扩展方面更是得心应手。

  • 建立一套触发机制

根据单一职责原则,每个类的职责是单一的,那么怎么把各个单一的职责串联成真实世界的复杂的逻辑关系呢?比如,我们去打猎,打死了一只母鹿,母鹿有三个幼崽,因失去了母鹿而饿死,尸体又被两只秃鹰争抢,因分配不均,秃鹰开始斗殴,然后羸弱的秃鹰死掉, 生存下来的秃鹰,则因此扩大了地盘……这就是一个触发机制,形成了一个触发链。观察者模式可以完美地实现这里的链条形式。

22.3.2 观察者模式的缺点

观察者模式需要考虑一下开发效率和运行效率问题,一个被观察者,多个观察者,开发和调试就会比较复杂,而且在Java中消息的通知默认是顺序执行,一个观察者卡壳,会影响整体的执行效率。在这种情况下,一般考虑采用异步的方式。

多级触发时的效率更是让人担忧,大家在设计时注意考虑。

22.3.3 观察者模式的使用场景

  • 关联行为场景。需要注意的是,关联行为是可拆分的,而不是“组合”关系。
  • 事件多级触发场景。
  • 跨系统的消息交换场景,如消息队列的处理机制。

22.3.4 观察者模式的注意事项

使用观察者模式也有以下两个重点问题要解决。

  • 广播链的问题

如果你做过数据库的触发器,你就应该知道有一个触发器链的问题,比如表A上写了一个触发器,内容是一个字段更新后更新表B的一条数据,而表B上也有个触发器,要更新表C,表C也有触发器……完蛋了,这个数据库基本上就毁掉了!我们的观察者模式也是一样的问题,一个观察者可以有双重身份,既是观察者,也是被观察者,这没什么问题呀,但是链一旦建立,这个逻辑就比较复杂,可维护性非常差,根据经验建议,在一个观察者模式中最多出现一个对象既是观察者也是被观察者,也就是说消息最多转发一次(传递两次),这还是比较好控制的。


注意 它和责任链模式的最大区别就是观察者广播链在传播的过程中消息是随时更改的,它是由相邻的两个节点协商的消息结构;而责任链模式在消息传递过程中基本上保持消息不可变,如果要改变,也只是在原有的消息上进行修正。


  • 异步处理问题

这个EJB是一个非常好的例子,被观察者发生动作了,观察者要做出回应,如果观察者比较多,而且处理时间比较长怎么办?那就用异步呗,异步处理就要考虑线程安全和队列的问题,这个大家有时间看看Message Queue,就会有更深的了解。

21.5 最佳实践

组合模式在项目中到处都有,比如现在的页面结构一般都是上下结构,上面放系统的Logo,下边分为两部分:左边是导航菜单,右边是展示区,左边的导航菜单一般都是树形的结构,比较清晰,有非常多的JavaScript源码实现了类似的树形菜单,大家可以到网上搜索一下。

还有,大家常用的XML结构也是一个树形结构,根节点、元素节点、值元素这些都与我们的组合模式相匹配,之所以本章节不以XML为例子讲解,是因为很少有人还直接读写XML文件,一般都是用JDOM或者DOM4J了。

还有一个非常重要的例子:我们自己本身也是一个树状结构的一个树枝或树叶。根据我能够找到我的父母,根据父亲又能找到爷爷奶奶,根据母亲能够找到外公外婆等,很典型的树形结构,而且还很规范(这个要是不规范那肯定乱套了)。

21.4 组合模式的扩展

21.4.1 真实的组合模式

什么是真实的组合模式?就是你在实际项目中使用的组合模式,而不是仅仅依照书本上学习到的模式,它是“实践出真知”。在我们的例子中,经过精简后,确实是类、接口减少了很多,而且程序也简单很多,但是大家可能还是很迷茫,这个Client程序并没有改变多少呀,非常正确,树的组装是跑不了的,你要知道在项目中使用关系型数据库来存储这些信息,你可以从数据库中直接提取出哪些人要分配到树枝,哪些人要分配到树叶,树枝与树枝、树叶的关系等,这些都是由相关的业务人员维护到数据库中的,通常这里是把数据存放到一张单独的表中,表结构如图21-7所示。

image-20210929172935715

图21-7 关系数据库中存储的树形结构

这张数据表定义了一个树形结构,我们要做的就是从数据库中把它读取出来,然后展现到前台上,用for循环加上递归就可以完成这个读取。用了数据库后,数据和逻辑已经在表中定义好了,我们直接读取放到树上就可以了,这个还是比较容易做的,大家不妨自己考虑一下。

这才是组合模式的真实引用,它依靠了关系数据库的非对象存储性能,非常方便地保存了一个树形结构。大家可以在项目中考虑采用,想想看现在还有哪个项目不使用关系型数据库呢?

21.4.2 透明的组合模式

组合模式有两种不同的实现:透明模式和安全模式,我们上面讲的就是安全模式,那透明模式是什么样子呢?透明模式的通用类图,如图21-8所示。

image-20210929173044473

图21-8 透明模式的通用类图

我们与图21-6所示的安全模式类图对比一下就非常清楚了,透明模式是把用来组合使用的方法放到抽象类中,比如add()、remove()以及getChildren等方法(顺便说一下,getChildren 一般返回的结果为Iterable的实现类,很多,大家可以看JDK的帮助),不管叶子对象还是树枝对象都有相同的结构,通过判断是getChildren的返回值确认是叶子节点还是树枝节点,如果处理不当,这个会在运行期出现问题,不是很建议的方式;安全模式就不同了,它是把树枝节点和树叶节点彻底分开,树枝节点单独拥有用来组合的方法,这种方法比较安全,我们的例子使用了安全模式。

由于透明模式的使用者还是比较多,我们也把它的通用源代码共享出来,首先看抽象构件,如代码清单21-22所示。

代码清单21-22 抽象构件

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class Component {
//个体和整体都具有的共享
public void doSomething(){
//编写业务逻辑
}
//增加一个叶子构件或树枝构件
public abstract void add(Component component);
//删除一个叶子构件或树枝构件
public abstract void remove(Component component);
//获得分支下的所有叶子构件和树枝构件
public abstract ArrayList<Component> getChildren();
}

抽象构件定义了树枝节点和树叶节点都必须具有的方法和属性,这样树枝节点的实现就不需要任何变化,如代码清单21-19所示。

树叶节点继承了Component抽象类,不想让它改变有点难,它必须实现三个抽象方法, 怎么办?好办,给个空方法,如代码清单21-23所示。

代码清单21-23 树叶节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Leaf extends Component {
@Deprecated
public void add(Component component) throws UnsupportedOperationException{
//空实现,直接抛弃一个"不支持请求"异常
throw new UnsupportedOperationException();
}
@Deprecated
public void remove(Component component)throws UnsupportedOperationException{
//空实现
throw new UnsupportedOperationException();
}
@Deprecated
public ArrayList<Component> getChildren()throws UnsupportedOperationException{
//空实现
throw new UnsupportedOperationException();
}
}

为什么要加个Deprecated注解呢?就是在编译器期告诉调用者,你可以调我这个方法, 但是可能出现错误哦,我已经告诉你“该方法已经失效”了,你还使用那在运行期也会抛出UnsupportedOperationException异常。

在透明模式下,遍历整个树形结构是比较容易的,不用进行强制类型转换,如代码清单21-24所示。

代码清单21-24 树结构遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Client {
//通过递归遍历树
public static void display(Component root){
for(Component c:root.getChildren()){
if(c instanceof Leaf){
//叶子节点
c.doSomething();
}
else{
//树枝节点
display(c);
}
}
}
}

仅仅在遍历时不再进行牵制的类型转化了,其他的组装则没有任何变化。透明模式的好处就是它基本遵循了依赖倒转原则,方便系统进行扩展。

21.4.3 组合模式的遍历

我们在上面也还提到了一个问题,就是树的遍历问题,从上到下遍历没有问题,但是我要是从下往上遍历呢?比如组织机构这棵树,我从中抽取一个用户,要找到它的上级有哪些,下级有哪些,怎么处理?想想,再想想!想出来了吧,我们对下答案,类图如图21-9所示。

image-20210929173339475

图21-9 增加父查询的类图
看类图中,在Corp类中增加了两个方法,setParent是设置父节点是谁,getParent是查找父节点是谁,我们来看一下程序的改变,如代码清单21-25所示。

代码清单21-25 抽象构件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public abstract class Corp {
//公司每个人都有名称
private String name = "";
//公司每个人都职位
private String position = "";
//公司每个人都有薪水
private int salary =0;
//父节点是谁
private Corp parent = null;
public Corp(String _name,String _position,int _salary){
this.name = _name;
this.position = _position;
this.salary = _salary;
}
//获得员工信息
public String getInfo(){
String info = "";
info = "姓名:" + this.name;
info = info + "\t职位:"+ this.position;
info = info + "\t薪水:" + this.salary;
return info;
}
//设置父节点
protected void setParent(Corp _parent){
this.parent = _parent;
}

//得到父节点
public Corp getParent(){
return this.parent;
}
}

就增加了粗体部分,然后我们再来看看树枝节点的改变,如代码清单21-26所示。

代码清单21-26 树枝构件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Branch extends Corp {
//领导下边有哪些下级领导和小兵
ArrayList<Corp> subordinateList = new ArrayList<Corp>();
//构造函数是必需的
public Branch(String _name,String _position,int _salary){
super(_name,_position,_salary);
}
//增加一个下属,可能是小头目,也可能是个小兵
public void addSubordinate(Corp corp) {
corp.setParent(this);
//设置父节点
this.subordinateList.add(corp);
}
//我有哪些下属
public ArrayList<Corp> getSubordinate() {
return this.subordinateList;
}
}

增加了粗体部分。看懂程序了吗?甭管是树枝节点还是树叶节点,在每个节点都增加了一个属性:父节点对象,这样在树枝节点增加子节点或叶子节点是设置父节点,然后你看整棵树除了根节点外每个节点都有一个父节点,剩下的事情还不好处理吗?每个节点上都有父节点了,你要往上找,那就找呗!大家自己考虑一下,写个find方法,然后一步一步往上找,非常简单的方法,这里就不再赘述。

有了这个parent属性,什么后序遍历(从下往上找)、中序遍历(从中间某个环节往上或往下遍历)都解决了,这个就不多说了。

再提一个问题,树叶节点和树枝节点是有顺序的,你不能乱排,怎么办?比如我们上面的例子,研发一组下边有3个成员,这3个成员要进行排序(在机关里这叫做排位,同样是同事也有个先后升迁顺序),你怎么处理?问我呀,问你呢,好好想想,以后用得着的!

21.3 组合模式的应用

21.3.1 组合模式的优点

  • 高层模块调用简单

一棵树形机构中的所有节点都是Component,局部和整体对调用者来说没有任何区别, 也就是说,高层模块不必关心自己处理的是单个对象还是整个组合结构,简化了高层模块的代码。

  • 节点自由增加

使用了组合模式后,我们可以看看,如果想增加一个树枝节点、树叶节点是不是都很容易,只要找到它的父节点就成,非常容易扩展,符合开闭原则,对以后的维护非常有利。

21.3.2 组合模式的缺点

组合模式有一个非常明显的缺点,看到我们在场景类中的定义,提到树叶和树枝使用时的定义了吗?直接使用了实现类!这在面向接口编程上是很不恰当的,与依赖倒置原则冲突,读者在使用的时候要考虑清楚,它限制了你接口的影响范围。

21.3.3 组合模式的使用场景

  • 维护和展示部分-整体关系的场景,如树形菜单、文件和文件夹管理。
  • 从一个整体中能够独立出部分模块或功能的场景。

21.3.4 组合模式的注意事项

只要是树形结构,就要考虑使用组合模式,这个一定要记住,只要是要体现局部和整体的关系的时候,而且这种关系还可能比较深,考虑一下组合模式吧。

20.3 迭代器模式的应用

我们在例子中使用了迭代器模式后为什么使原本简单的应用变得复杂起来了呢?那是因为我们在简单的应用中使用了迭代器,在哪?请看代码清单20-3,注意这段话:for(IProject project:projectList),它为什么能够运行起来?还不是因为ArrayList已经实现了iterator()方法, 我们才能如此简单地应用。

从JDK 1.2版本开始增加java.util.Iterator这个接口,并逐步把Iterator应用到各个聚集类 (Collection)中,我们来看JDK 1.5的API帮助文件,你会看到有一个叫java.util.Iterable的接口,看看有多少个接口继承了它:

1
BeanContext,BeanContextServices,BlockingQueue<E>,Collection<E>,List<E>,Queue<E>,Set<E>,SortedSet<E>

, 再看看有它多少个实现类:

1
AbstractCollection,AbstractList,AbstractQueue,AbstractSequentialList,AbstractSet,ArrayBlockingQueue,ArrayList,AttributeList,BeanContextServicesSupport,BeanContextSupport,ConcurrentLinkedQueue,CopyOnWriteArrayList,CopyOnWriteArraySet,DelayQueue,EnumSet,HashSet,JobStateReasons,LinkedBlockingQueue,LinkedHashSet,LinkedList,PriorityBlockingQueue,PriorityQueue,RoleList,RoleUnresolvedList,Stack,SynchronousQueue,TreeSet,Vector

, 基本上我们经常使用的类都在这个表中了,也正是因为Java把迭代器模式已经融入到基本 API中了,我们才能如此轻松、便捷。

我们再来看看Iterable接口。java.util.Iterable接口只有一个方法:iterator(),也就说,通过iterator()这个方法去遍历聚集类中的所有方法或属性,基本上现在所有的高级语言都有Iterator这个接口或者实现,Java已经把迭代器给我们准备好了,我们再去写迭代器,就有点多余了。所以呀,这个迭代器模式也有点没落了,基本上很少有项目再独立写迭代器了,直接使用Collection下的实现类就可以完美地解决问题。

迭代器现在应用得越来越广泛了,甚至已经成为一个最基础的工具。一些大师级人物甚至建议把迭代器模式从23个模式中删除,为什么呢?就是因为现在它太普通了,已经融入到各个语言和工具中了,比如PHP中你能找到它的身影,Perl也有它的存在,甚至是前台的页面技术AJAX也可以有它的出现(如在Struts2中就可以直接使用iterator)。基本上,只要你不是在使用那些古董级(指版本号)的编程语言的话,都不用自己动手写迭代器。

19.5 最佳实践

适配器模式是一个补偿模式,或者说是一个“补救”模式,通常用来解决接口不相容的问题,在百分之百的完美设计中是不可能使用到的,什么是百分之百的完美设计?“千虑”而没有“一失”的设计,但是,再完美的设计也会遇到“需求”变更这个无法逃避的问题,就以我们上面的人力资源管理系统为例来说,不管系统设计得多么完美,都无法逃避新业务的发生, 技术只是一个工具而已,是因为它推动了其他行业的进步和发展而具有了价值,通俗地说, 技术是为业务服务的,因此业务在日新月异变化的同时,也对技术提出了同样的要求,在这种要求下,就需要我们有一种或一些这样的补救模式诞生,使用这些补救模式可以保证我们的系统在生命周期内能够稳定、可靠、健壮的运行,而适配器模式就是这样的一个“救世主”,它在需求巨变、业务飞速而导致你极度郁闷、烦躁、崩溃的时候横空出世,它通过把非本系统接口的对象包装成本系统可以接受的对象,从而简化了系统大规模变更风险的存在。

19.4 适配器模式的扩展

我们刚刚讲的人力资源管理的例子中,其实是一个比较幸运的例子,为什么呢?如果劳动服务公司提供的人员接口不止一个,也就是说,用户基本信息是一个接口,工作信息是一个接口,家庭信息是一个接口,总共有三个接口三个实现类,想想看如何处理呢?不能再使用我们上面的方法了,为什么呢?Java是不支持多继承的,你难道想让OuterUserInfo继承三个实现类?此路不通,再想一个办法,对哦,可以使用类关联的办法嘛!声明一个OuterUserInfo实现类,实现IUserInfo接口,通过再关联其他三个实现类不就可以解决这个问题了吗?是的,是的,好方法,我们先画出类图,如图19-8所示。

OuterUserInfo通过关联的方式与外界的三个实现类通讯,当然也可以理解为是聚合关系。IUserInfo和UserInfo代码如代码清单19-1和代码清单19-2所示,不再赘述。我们来看看拆分后的三个接口和实现类,用户基本信息接口如代码清单19-13所示。

image-20210929163847652

图19-8 拆分接口后的类图

代码清单19-13 用户基本信息接口

1
2
3
4
public interface IOuterUserBaseInfo {
//基本信息,比如名称、性别、手机号码等
public Map getUserBaseInfo();
}

用户家庭信息接口如代码清单19-14所示。

代码清单19-14 用户家庭信息接口

1
2
3
4
5
public interface IOuterUserHomeInfo {
//用户的家庭信息
public Map getUserHomeInfo();

}

用户工作信息接口如代码清单19-15所示。

代码清单19-15 用户工作信息接口

1
2
3
4
public interface IOuterUserOfficeInfo {
//工作区域信息
public Map getUserOfficeInfo();
}

读到这里,读者应该想到这样一个问题:系统这样设计是否合理呢?合理,绝对合理! 想想单一职责原则是怎么说的,类和接口要保持职责单一,在实际的应用中类可以有多重职责,但是接口一定要职责单一,因此,我们上面拆分接口的假想也是非常合乎逻辑的。我们来看三个相关的实现类,用户基本信息如代码清单19-16所示。

代码清单19-16 用户基本信息

1
2
3
4
5
6
7
8
9
10
11
public class OuterUserBaseInfo implements IOuterUserBaseInfo {
/*
* 用户的基本信息
*/
public Map getUserBaseInfo() {
HashMap baseInfoMap = new HashMap();
baseInfoMap.put("userName", "这个员工叫混世魔王...");
baseInfoMap.put("mobileNumber", "这个员工电话是...");
return baseInfoMap;
}
}

用户家庭信息如代码清单19-17所示。

代码清单19-17 用户家庭信息

1
2
3
4
5
6
7
8
9
10
11
public class OuterUserHomeInfo implements IOuterUserHomeInfo {
/*
* 员工的家庭信息
*/
public Map getUserHomeInfo() {
HashMap homeInfo = new HashMap();
homeInfo.put("homeTelNumbner", "员工的家庭电话是...");
homeInfo.put("homeAddress", "员工的家庭地址是...");
return homeInfo;
}
}

用户工作信息如代码清单19-18所示。

代码清单19-18 用户工作信息

1
2
3
4
5
6
7
8
9
10
11
public class OuterUserOfficeInfo implements IOuterUserOfficeInfo {
/*
* 员工的工作信息,比如,职位等
*/
public Map getUserOfficeInfo() {
HashMap officeInfo = new HashMap();
officeInfo.put("jobPosition","这个人的职位是BOSS...");
officeInfo.put("officeTelNumber", "员工的办公电话是...");
return officeInfo;
}
}

这里又到我们的核心了——适配器。好,我们来看适配器代码,如代码清单19-19所示。

代码清单19-19 适配器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class OuterUserInfo implements IUserInfo {
//源目标对象
private IOuterUserBaseInfo baseInfo = null;
//员工的基本信息
private IOuterUserHomeInfo homeInfo = null;
//员工的家庭信息
private IOuterUserOfficeInfo officeInfo = null;
//工作信息
//数据处理
private Map baseMap = null;
private Map homeMap = null;
private Map officeMap = null;
//构造函数传递对象
public OuterUserInfo(IOuterUserBaseInfo _baseInfo,IOuterUserHomeInfo _homeInfo,IOuterUserOfficeInfo _officeInfo){
this.baseInfo = _baseInfo;
this.homeInfo = _homeInfo;
this.officeInfo = _officeInfo;
//数据处理
this.baseMap = this.baseInfo.getUserBaseInfo();
this.homeMap = this.homeInfo.getUserHomeInfo();
this.officeMap = this.officeInfo.getUserOfficeInfo();
}
//家庭地址
public String getHomeAddress() {
String homeAddress = (String)this.homeMap.get("homeAddress");
System.out.println(homeAddress);
return homeAddress;
}
//家庭电话号码
public String getHomeTelNumber() {
String homeTelNumber = (String)this.homeMap.get("homeTelNumber");
System.out.println(homeTelNumber);
return homeTelNumber;

}
//职位信息
public String getJobPosition() {
String jobPosition = (String)this.officeMap.get("jobPosition");
System.out.println(jobPosition);
return jobPosition;
}
//手机号码
public String getMobileNumber() {
String mobileNumber = (String)this.baseMap.get("mobileNumber");
System.out.println(mobileNumber);
return mobileNumber;
}
//办公电话
public String getOfficeTelNumber() {
String officeTelNumber= (String)this.officeMap.get("officeTelNumber");
System.out.println(officeTelNumber);
return officeTelNumber;
}
// 员工的名称
public String getUserName() {
String userName = (String)this.baseMap.get("userName");
System.out.println(userName);
return userName;
}
}

大家只要注意一下黑色字体的构造函数就可以了,它接收三个对象,其他部分变化不大,只是变量名称进行了修改,我们再来看场景类,如代码清单19-20所示。

代码清单19-20 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Client {
public static void main(String[] args) {
//外系统的人员信息
IOuterUserBaseInfo baseInfo = new OuterUserBaseInfo();
IOuterUserHomeInfo homeInfo = new OuterUserHomeInfo();
IOuterUserOfficeInfo officeInfo = new OuterUserOfficeInfo();
//传递三个对象
IUserInfo youngGirl = new OuterUserInfo(baseInfo,homeInfo,officeInfo);
//从数据库中查到101个
for(int i=0;i<101;i++){
youngGirl.getMobileNumber();
}
}
}

运行的结果还是相同的。大家想想看,OuterUserInfo变成了委托服务,把IUserInfo接口需要的所有的操作都委托给其他三个接口下的实现类,它的委托是通过对象层次的关联关系进行委托的,而不是继承关系。好了,讲了这么多,我们需要给这种适配器起个名字,就是对象适配器,我们之前讲的通过继承进行的适配,叫做类适配器。对象适配器的通用类图, 如图19-9所示。

image-20210929164327675

图19-9 对象适配器类图
适配器的通用代码也比较简单,把原有的继承关系变更为关联关系就可以了,不再赘述。对象适配器和类适配器的区别是:类适配器是类间继承,对象适配器是对象的合成关系,也可以说是类的关联关系,这是两者的根本区别。二者在实际项目中都会经常用到,由于对象适配器是通过类间的关联关系进行耦合的,因此在设计时就可以做到比较灵活,比如修补源角色的隐形缺陷,关联其他对象等,而类适配器就只能通过覆写源角色的方法进行扩展,在实际项目中,对象适配器使用到场景相对较多。

19.3 适配器模式的应用

19.3.1 适配器模式的优点

  • 适配器模式可以让两个没有任何关系的类在一起运行,只要适配器这个角色能够搞定他们就成。
  • 增加了类的透明性

想想看,我们访问的Target目标角色,但是具体的实现都委托给了源角色,而这些对高层次模块是透明的,也是它不需要关心的。

  • 提高了类的复用度

当然了,源角色在原有的系统中还是可以正常使用,而在目标角色中也可以充当新的演员。

  • 灵活性非常好

某一天,突然不想要适配器,没问题,删除掉这个适配器就可以了,其他的代码都不用修改,基本上就类似一个灵活的构件,想用就用,不想就卸载。

19.3.2 适配器模式的使用场景

适配器应用的场景只要记住一点就足够了:你有动机修改一个已经投产中的接口时,适配器模式可能是最适合你的模式。比如系统扩展了,需要使用一个已有或新建立的类,但这个类又不符合系统的接口,怎么办?使用适配器模式,这也是我们例子中提到的。

19.3.3 适配器模式的注意事项

适配器模式最好在详细设计阶段不要考虑它,它不是为了解决还处在开发阶段的问题, 而是解决正在服役的项目问题,没有一个系统分析师会在做详细设计的时候考虑使用适配器模式,这个模式使用的主要场景是扩展应用中,就像我们上面的那个例子一样,系统扩展了,不符合原有设计的时候才考虑通过适配器模式减少代码修改带来的风险。

再次提醒一点,项目一定要遵守依赖倒置原则和里氏替换原则,否则即使在适合使用适配器的场合下,也会带来非常大的改造。

18.5 最佳实践

策略模式是一个非常简单的模式。它在项目中使用得非常多,但它单独使用的地方就比较少了,因为它有致命缺陷:所有的策略都需要暴露出去,这样才方便客户端决定使用哪一个策略。例如,在例子中的赵云,实际上不知道使用哪个策略,他只知道拆第一个锦囊,而不知道是BackDoor这个妙计。是的,诸葛亮已经在规定了在适当的场景下拆开指定的锦囊, 我们的策略模式只是实现了锦囊的管理,但是我们没有严格地定义“适当的场景”拆开“适当的锦囊”,在实际项目中,我们一般通过工厂方法模式来实现策略类的声明,读者可以参考混编模式。