7 多个对象组成结构
7.1 过程描述
上一节介绍了如何创建一个对象。但大多数情况,一个对象是不够用的,这时候就需要把对象包装、封装、多对象组合。有时候还需要将一个组合作为一个整体使用,组合要提供对外的接口,也可能会用到系统原有的接口。
下面针对每种情况详细介绍。
7.2 情况1:借用外部接口
有开发经验的人知道,日常大部分开发都是在已有系统基础上开发的。即便是从新开发的系统,也要依赖于一个框架或者库。
所以,我们每时每刻都在用系统已有的接口。但是如果这些接口不满足我们的需求,我们就需要重新对接口封装一下,让其符合当前的规则。就是这个我们日常用的技巧,被GoF总结成为一个模式——适配器模式。
不用看代码和类图,也能明白它的意思。不必太计较代码和类图的细节,重点在于理解设计思想。
顾名思义,适配器就是做一个隔离,起到了解耦的作用。例如我们日常用的笔记本电脑适配器。
7.3 情况2:给对象增加新功能
系统总是在不断的维护和升级当中,也可能在不断的需求变更当中,因此为对象增加新功能,是再常见不过的了。那么如何为对象增加新功能呢?
最直接的回答就是改代码呗。改类型的代码,增加方法、属性等。
对于这种修改,首先想到的应该是违反了“开放封闭原则”和“单一职责原则”,违反这种原则带来的坏处很多。代码越改越多;每次更改都有可能影响以前代码;多人维护一个文件,不利于协同开发……
如果用“抽象”“隔离”的思想来思考这一问题,很容易就能找出思路:第一,把原有功能和新增功能隔离;第二,两者都依赖于一个抽象,这个抽象就是对象应该有的所有功能;第三,外部客户将依赖于抽象,它不会察觉内部的变化(依赖倒置原则)。
这就是装饰模式。
从上面的类图看,ConreteComponent是原始类型,Decorator是一个抽象类,它的派生类负责添加新功能。这里理解的重点,在于Decorator类中有一个Component属性,相当于Decorator封装了一个Component,直接调用他原有的功能,并且可以新增功能。当然,这些操作都是可以派生在子类中实现的。而且不同的子类可以实现增加不同的功能。
这样的抽象和分离就符合开放封闭原则和单一职责原则,也不会出现代码量过多、多人维护不便等问题。
7.4 情况3:封装功能
对于有些功能,我们不希望客户端直接调用,而是在调用时候先做一个判断,或者加一个缓存。其实就是在真实功能和客户端之间,加一个中间层。而这不能让客户端调用察觉。
如果你把这个中间层直接加入到真是功能中,虽然这可以不让客户端察觉,那将会给系统带来隐患,违反“单一职责原则”。如下:
首先,如何不让客户端察觉?答案很简单——依赖倒置原则——让客户端依赖于一个抽象。这个抽象将如何实现呢? 具体的实现和中间层都要去实现。如下:
类图如下:
这就是代理模式。
每个设计模式要体现的都是一种设计的思路,代理模式就是要在客户端和底层实现加一层,在该层中实现一些业务场景。4s店就是客户于汽车厂家的代理。
具体是否要都去实现同一个接口,这种细节不重要,不要去过于纠结这些类图和代码。
7.5 情况4:递归关系的组合
上文提到如何更有效率的维护对象功能和新增功能,以及更有效率的封装对象。这两种做法的输出,其实还是一个单个的对象。如何将一个个对象组合成一个视图,系统中最常见的无非是两种——列表、树,以及两者的结合体——TreeGrid
列表是比较简单的结构,按实际的需求应用,不会产生太多误解。而树结构却有值得讨论之处。最简单的树节点实现的代码如下:代码很简单,只有节点的名称,和对代码下级节点的管理。
如果我们应对的业务很简单,例如类似于windows系统的文件夹树,即每个节点的类型都一样,每个节点的功能也都一样,叶子节点和摘要节点在功能上没有区别。这种情况下,可以用以上代码轻松应对。
但是如果遇到以下情况呢,如下图:
这也是个树结构,但是每个节点类型都不一样,形式的功能也不一样,“个人”是个叶子节点,不能再添加下级节点。这种情况下,再用以上那段代码就会出现许多问题,如多个功能集中在一个代码文件中,多人维护一段代码等。
如何解决这一问题,组合模式给予我们灵感。
根据以上类图,可以看出组合模式解决这一问题的思路是:将树结构中的节点的统一功能抽象出来,不同类型的节点,用不同的子类去实现。类图中只有两个子类,我们可以根据自己的实际情况来派生多个子类。
这样解释想必大部分人都能理解该模式的设计思路,不必再用代码挨着表达了。关键在于理解如何分析问题,如何抽象,如何隔离,如何解耦,最终就是如何设计。
这样设计符合开放封闭原则、职责单一原则,对于客户端也符合依赖倒置原则。
7.6 情况5:分离多层继承
在对象组合过程中,难免会出现继承的情况,甚至会出现多层继承。根据设计原则——少继承、多聚合。因此不建议我们使用多层继承。而是尽量把这种多层继承的关系,变成聚合的关系。
在一个多层继承结构中,如果底层节点可以抽象出相同的功能,即可变为聚合关系。如:
如上图,子类可以提取出一个抽象。变成这样的设计:
这样就把多继承变成了聚合。
可以总结归纳以下这种情况。我们把左侧“发送消息”及其子类叫做“抽象”,右侧的“发送方式”及其子类叫做“实现”。那么我们现在做的就是从“实现”和“抽象”的继承关系,变成了两者的聚合关系。
这就是——桥接模式。以下是类图:
他应对的场景是抽象有多样性,实现也有多样性。抽象的抽象只依赖于实现的抽象。从而解耦抽象和实现的关联。
7.7 情况6:封装组合,供外部使用
当一个组合封装完成后,要提供统一的接口供外部客户端使用。而不是让客户端在组合内部任意的调用。
这就是外观模式。很好理解,也经常用到,可能只是不知道这个名字而已。它像一个包袱一样包起来组合,只留规定的接口。
外观模式简单易懂,不需要类图和代码过多解释。
7.8 总结:
(注:未包括“Flyweight享元模式”。将在后续版本更新中加入。)
其实以上这几种情况,就是结构性的设计模式对应的问题,每种情况对应一种设计模式。结合自己或多或少的开发经验,仔细考虑分析这几种情况,肯定每种情况都是你在编码中遇到的,也是一个对象组合很可能需要的。
遇到这些问题时,你当时是怎么解决的?不一定非得按照设计模式上的解决方式。但是要已定符合设计原则。设计模式只是一个“术”,提供一个解决思路或者灵感,而设计原则、设计思想才是“道”。