# 13.2 结构模式——建筑的技巧
# 层与区
程序设计的关键是控制软件的复杂性,而控制复杂性最常见的技巧莫过于分治法(divide and conquer/rule)
如在宏观的架构设计上,通常会把系统分为若干层和若干区。
- 区别
区别之一是划分标准,层是按抽象层次进行的水平划分,区是同一抽象层中按功能领域进行的垂直划分,如业务层中按照业务类别分区。
区别之二是模块关系,不同层的子系统是单向依赖的关系,即高层建立于低层之上;相同层不同区的子系统是双向合作的平等关系。
在OOP中,给定某一层的某一区,通常与某个包(C++、C#中的namespace,Java中的package)相对应。
具体到某个包,可更细致地划分为若干子包,如此递归下去,直到最基本的模块单位——类为止。
- 对付软件复杂性,除了抽象与分解两个基本原则外,还可以划分层级。
类型层级 或 泛化层级 是OOP中特有的一类层级,又称 is-a 或 kind-of 层级。
描述的是继承关系,或 ‘泛化-特化’(generalization/specialization)关系。
非OOP专有的聚合层级(aggregation hierarchy),又称 has-a 或 part-of 层级。
描述的是包含关系,或者‘整体-部分’(whole/part)关系。
- 所谓层级,实质也是一种抽象,是抽象层次的分级。
层级应用于模块架构,例如N层架构;
应用于类结构,便是类型层级;
用于对象结构,便是聚合层级。
一个系统设计是否合理,很大程度上取决于层级的设计。
- 抽象是个相对的概念,不是越抽象越好,关键要适度。
过分抽象因内容空泛而缺乏有效的实用性,过分具体则因条件严苛而缺乏广发的适用性。
所谓自底向上设计,即低层模块经过合作(collaboration)而产生高层模块
所谓自顶向下设计,即高层模块经过细化(refinement)而产生低层模块。
无论哪种设计,抽象层次都是向上逐步递增,不可随意越级调用。如,表现层访问业务层是合理,但访问更低层的数据层就值得商榷了。
之所以说业务层比数据层更抽象,是因为它把后者封装为自己的实现。
一旦破坏封装,他们的抽象层级的界限随之变得模糊,分层的意义也大打折扣。
此外,每个模块的抽象层级都应与其所在的层级相匹配,不宜过高,也不宜过低。
事实上,强类型系统的一大作用就是一定程度上避免了不同抽象层次的混用。
难怪不提倡向下转型,就是因为它试图把高抽象类型转化为低抽象类型,从而破坏了抽象层次的一致性。
破坏了抽象层次的一致性,等于破坏了抽象,因为抽象是需要参照物的,就是它所在的层次。
类似的,Java程序中可以利用JNI(Java Native Interface)与C程序交互,混用不同抽象层次的语言将令程序丧失可移植性。
* 分解的目的是降低复杂性,以便分而治之。
```md
分解而得的模块应满足低耦高聚,但模块之间的联系也是不可或缺的,否则无法形成有效的合作。
由于每个模块代表一种抽象,它们之间的关联尝尝一层级的形式出现。
在OOP中类级别最常用的层级便是类型层级和聚合层级。
- 结构模式(structural pattern)
它关注的是如何把类和对象组合成更大的结构,
实质上就是一个利用继承层级的类结构和聚合层级的对象结构来构建更高层抽象的过程。
- 桥梁模式
P413 图13-4
桥梁模式的重点是分解,让本来结合紧密的接口和实现分离开来;
桥梁模式通常是事先的有意设计,当然也不排除事后的代码重构的可能。更多地出自可维护性的考虑。
- 适配器模式(adapter)
是一个接口转换器,可以解决服务提供者与服务享受者之间接口不兼容的问题。
适配器模式的重点是结合,让本来无关的类能合作共事。
适配器模式通常是事后补救,更多地出自可重用性的考量。
比如为了重用第三方代码或遗留(legacy)代码,或让来自不同开发组的代码能协作等。
不过有意而为之的情况也非罕见,比如遵循迪米特法则,尽量缩小类的交际圈,往往特意用适配器类作为中间媒介。
适配器模式是对原有接口的一种包装,所以也称包装模式(wrapper pattern),
它也是另一个模式的别名,即装饰者模式(decorator pattern)。这里的装饰是指为一个对象增加或修改某些职责或行为。
- 装饰者模式
装饰者模式在类型层级和聚合层级有交合的部分。其中类型层级在编译器决定,聚合层级是动态生成的。
P415 图13-7
装饰者模式不仅能动态增加对象的职责,也能动态地取消职责。
有效分离类的核心功能与边缘功能,符合关注点分离原则。
装饰的边缘性往往体现在代码实现上,通常装饰者在覆盖父类的方法时,仍能保留其核心代码,只是在前后增加一些语句。
同样是对对象的包装,适配器模式改变对象的接口而保持其职责,装饰者模式改变对象的职责而保持其接口。
```md
* 代理模式
```md
代理模式与装饰者模式一样也保持对象的职责,但它可能限制接口的服务。
代理的目的是为了控制客户对某个对象的访问,为其提供一个代理或预留位置。
如果某个对象的初始化十分耗时间或资源,却又未必立即投入使用,变可以利用代理来推迟创建(lazy initialization)。
如,页面中的图形部分可能暂时预留位置或仅显示缩略图(thumbnail),知道用户点击时才生成实际图形。
在ORM框架中,加载一个域数据库对应的实体对象时,
可能返回一个代理对象,以减少对数据库的访问,这里的代理有一个名字——虚拟代理。
远程代理多用于分布式系统,它把远程机器上的某个对象用本地机器上的对象(stub)来表示。
代理机制是一种抽象机制,
虚拟代理掩盖的细节是:对象是否真正创建;
远程代理掩盖的细节是:对象是否在本地创建。
智能引用(smart reference)
C++中的智能指针式一种抽象化的指针,它通过重载指针运算符*和->来实现自动垃圾回收或边界检查,以弥补原始指针的不足。
它属于更广义的的一种代理——智能引用(smart reference)
对支持垃圾回收的Java和C#语言仍然有价值
保证互斥(mutual exclusion)——同步代理(synchronization proxcy)、
进行引用计数(reference counting)——计数代理(counting proxy)、
写时拷贝(copy-on-write,COW)或迟拷贝、
延迟加载持久化等。
保护代理(protected proxy)
可控制对原有对访问,以保障敏感数据的安全。
此外还有缓存代理、防火墙代理等。
- 外观模式(facade pattern)
P418 图13-10
采用外观模式,客户不用再与各种不同类打交道,只须一个统一的接口进行通信,符合迪米特法则;
而子系统中个服务类发生的变化不容易影响客户,符合保变原则。
它与装饰者模式、代理模式的不同之处在于:它改变被包装对象的接口,而后两者不变。
它与适配器模式的不同之处在于:
它是为了提供一种层次更高的抽象和粒度更粗的服务,而适配器通常只是单纯作为接口转换。
- 复合模式(composite pattern)
P419 图13-11
与装饰者模式一样,都是利用类型层级和聚合层级来构造更大的复合接口。
如果把符合模式中的Composite与Component之间一对多的关系限定为一对一的关系,
那么它将与装饰者模式在形式上无所分别,都是开放式的递归合成结构。
但二者的动机迥异:
装饰者模式是为了动态增减功能,同时便面子类膨胀,因此擦用的方法是:化继承为合成,化静态为动态;
符合模式是为了抽象理解和统一管理类型不同、数量众多的树形结构,因此采用的方法是:化不同为相同,化多个为一个。
- 享元模式(flyweight pattern)
在GoF的23个设计模式中,唯一一个主要针对软件的运行质量而非设计质量的模式是享元模式(flyweight pattern)
该模式针对一类颗粒度小、数量众多、相似度高、种类有限、具有值语义的对象,通过共享机制来减少对象的创建,以提供时空性能上的改善。
享元模式的关键抽象出一类对象内在的、不因环境而异(context-insensitive)的状态,封装后作为共享单元——flyweight。
但客户需要承担更多的职责——储存或计算享元的上下文信息,,否则对象将因数据不足而无法提供服务。
P423 图13-13 享元模式