引言
本部分参考黑皮书 GoF《设计模式》
# Chapter 1 引言
# 1.1 什么是设计模式
设计模式是一套架构设计经验,是对用来在特定场景下解决一般设计问题的类和相互通信的对象的描述。
一般而言,一个模式有四个基本要素:
- 模式名称 — 一个助记名,它用一两次来描述模式的问题、解决方案和效果。
- 问题 — 描述了应该在何时使用模式。解释了设计问题和问题存在的前因后果。
- 解决方案 — 描述了设计的组成成分,它们之间的相互关系及各自的职责和协作方式。
- 效果 — 描述了模式应用的效果及使用模式应权衡的问题。(包括它对系统的灵活性、扩充性或可移植性)
# 1.2 设计模式的组织与编目
根据目的准则,即模式是用来完成什么工作的,可分为:
- 创建型:与对象的创建有关
- 结构型:处理类或对象的组合
- 行为型:对类或对象怎样交互和怎样分配职责进行描述
根据范围准则,即指定模式主要是用于类还是用于对象,可分为:
- 类模式:处理类和子类之间的关系,这些关系通过继承建立,是静态的,在编译时刻便确定下来了
- 对象模式:处理对象之间的关系,这些关系在运行时刻是可以变化的,更具有动态性
# 1.3 设计模式怎样解决设计问题
# 1.3.1 寻找合适的对象
对象包括数据和对数据进行操作的方法。面向对象设计最困难的部分是通过考虑各种因素来将系统分解成对象集合。设计模式可以帮你确定并不明显的抽象和描述这些抽象的对象。
# 1.3.2 决定对象的粒度
对象在大小和数目上变化极大,设计模式很好地讲述了这个问题。
# 1.3.3 指定对象的接口
接口(interface)是对象操作所定义的所有方法签名的集合。类型(type)是一个用来标识特定接口的名字。当一个类型的接口包含了另一个类型的接口时,我们就说它是另一个类型的子类型,而称另一个类型是它的超类型。
动态绑定是指在运行期间(非编译期)根据具体的对象来确定出它所执行的方法的操作。动态绑定允许你在运行时彼此替换有相同接口的对象,这种可替换性就称为多态。
设计模式通过确定接口的主要组成成分及经接口发送的数据类型来帮助你定义接口,甚至还指定了接口之间的关系。
# 1.3.4 描述对象的实现
类指定了对象的内部数据和表示,也定义了对象所能完成的操作,也就是说类决定了对象的实现。
- 类名用黑体表示;操作在类名下面,成员数据在操作下面。
对象通过类的实例化来创建。虚箭头表示一个类实例化了一个对象:
类可以继承,我们以竖线和三角表示子类关系:
抽象类的主要目的是为它的子类定义公共接口,抽象类定义却没有实现的操作被称为抽象操作,非抽象类称为具体类。抽象类的类名和抽象操作以斜体表示:
混入类(mixin class)是给其他类提供可选择的接口或功能的类。它与抽象类一样不能实例化,混入类要求多继承。图示如下:
# 1)类继承 versus 接口继承
理解对象的类(class)和它的类型(type)是很重要的:
An object's class defines how the object is implemented. The class defines the object's internal state and the implementation of its operations. In contrast, an object's type only refers to its interface—the set of requests to which it can respond. An object can have many types, and objects of different classes can have the same type.
理解类继承(class inheritance)和接口继承(interface inheritance or subtyping)之间的差别也十分重要:
Class inheritance defines an object's implementation in terms of another object's implementation. In short, it's a mechanism for code and representation sharing. In contrast, interface inheritance (or subtyping) describes when an object can be used in place of another.
因为很多语言不显式区分这两个概念,所以容易被混淆。在 C++ 中,继承既指接口的继承又指实现的继承。C++ 中接口继承的标准方法是公有继承一个含(纯)虚成员函数的类(这样便可以代替其父类来执行这个方法)。C++ 中纯接口继承接近于公有继承纯抽象类,纯实现继承或纯类继承接近于私有继承(这样便可以使用父类的实现)。
# 2)针对接口编程,而不是针对实现编程
当继承被恰当使用时,所有从抽象类导出的类将共享该抽象类的接口。这时,所有的子类都能响应抽象类接口中的请求,从而子类的类型都是抽象类的子类型。
只根据抽象类中定义的接口来操纵对象有以下两个好处:
- Clients remain unaware of the specific types of objects they use, as long as(只要) the objects adhere to the interface that clients expect.
- Clients remain unaware of the classes that implement these objects. Clients only know about the abstract class(es) defining the interface.
这将极大地减少子系统实现之间的相互依赖关系,也产生了可复用的面向对象设计的如下原则:针对接口编程,而不是针对实现编程。
不将变量声明为某个特定的具体类的实例对象,而是让他遵从抽象类所定义的接口,这是本书设计模式的一个常见主题。
# 1.3.5 运用复用机制
# 1)继承 versus 组合
OOP 中功能复用的两种最常用的技术是类继承和对象组合。
- 类继承允许你根据其他类的实现来定义一个新的类的实现,这种通过生成子类的复用被称为白箱复用,因为在继承中父类的内部细节对子类可见。
- 对象组合是说新的功能可以通过组装或组合对象来获得,这种复用风格被称为黑箱复用,因为对象的内部细节是不可见的。
继承和复用各有优缺点:
- 类继承是在编译时静态定义的,且可直接使用。
- 优点:可以较方便地改变被复用的实现,在子类种植需要重定义一些而不是全部操作。
- 缺点:继承对子类揭示了父类的实现细节,破坏了封装性,这导致父类实现中的任何变化必然会导致子类发送变化。
- 对象组合是通过获得对其他对象的引用而在运行时动态定义的。
- 优点:被组合的对象只能通过接口访问,因此并没有破坏封装性,只要类型一致,运行时还可以用一个对象来替代另一个对象;类和继承层次会保持较小规模,避免增长为不可控制的庞然大物。
- 缺点:相比继承多了一次间接调用,导致运行低效问题;动态、高度参数化的软件比静态软件更难于理解。
这导出了 OOP 设计的第二个原则:优先使用对象组合,而不是类继承。
# 2)委托
委托(delegation)是对象组合的特例。在委托方式下,有两个对象参与处理一个请求,接受请求的对象将操作委托给它的代理者(delegate)。
只有当使用委托使设计比较简单而不是更复杂时,它才是好的选择。
# 3)参数化类型
参数化类型(parameterized type),在 C++ 中实现为模板。本书的设计模式并没有与其相关的。
# 1.3.6 关联运行时和编译时的结构
一个面向对象程序运行时的结构通常与它的代码结构相差较大。考虑对象聚合和相识的差别以及他们在编译时和运行时的表示是多么不同。
- 聚合意味着一个对象拥有另一个对象或对另一个对象负责。聚合对象和其所有者具有相同的生命周期。
- 相识意味着一个对象仅仅知道一个对象。他们不为彼此负责,比聚合关系要弱。
普通的箭头表示相识,尾部带有菱形的箭头线表示聚合:
从根本上讲,是聚合还是相识是由你的意图而不是显式的语言机制来决定的。
# 1.3.7 设计应支持变化
获得最大限度复用的关键在与对新需求和已有需求发生变化时的预见性,要求你的系统设计能够相应地改进。每一种设计模式允许系统结构的某个方面的变化独立与其他方面,这样产生的系统对于某种特殊的变化将更加健壮。
下面阐述了一些导致重新设计的一般原因:
- 通过显式地指定一个类来创建对象:在创建对象时指定类名将使你受特定实现的约束而不是特定接口的约束。要避免这种情况,应该间接地创建对象。
- 对特殊操作的依赖:当为请求指定特殊操作时,完成该请求的方式便固定了下来,导致把代码写死了。
- 对硬件和软件平台的依赖:产生可移植性的问题。
- 对对象表示或实现的依赖:当对象变化时,对导致客户也必须相应地变化。对客户隐藏这些实现信息能阻止连锁变化。
- 算法依赖:算法变化时依赖该算法的对象也不得不变化,因此应该将有可能变化的算法孤立起来。
- 紧耦合:设计模式使用抽象耦合和分层技术来提高系统的松散耦合性。
- 通过生成子类来扩充功能:可见继承的缺点。
- 不能方便地对类进行修改:可能对类的任何改变会要求修改许多已存在的其他子类。设计模式提供了在这些情况下对类进行修改的建议。
# 1.4 怎样选择设计模式
- 考虑设计模式是如何解决设计问题的
- 浏览模式的意图
- 研究模式是怎样相互关联的
- 研究目的相似的模式
- 检查重新设计的原因
- 考虑你的设计中哪些可变
各设计模式允许的可变部分:
- 创建型
Abstract Factory (对象):产品对象家族
Factory Method:被实例化的子类
Builder (对象):如何创建一个组合对象
Prototype (对象):被实例化的类
Singleton (对象):一个类的唯一实例
- 结构型
Adapter (类 & 对象):对象的接口
Bridge (对象):对象的实现
Composite (对象):一个对象的结构和组成
Decorator (对象):对象的职责,不生成子类
Facade (对象):一个子系统的接口
Flyweight (对象):对象的储存开销
Proxy (对象):如何访问一个对象,该对象的位置
- 行为型
Interpreter (类):一个语言的文法及解释
Template Method (类):算法中的有些步骤
Chain of Responsibility (对象):满足一个请求的对象
Command (对象):何时,怎样满足一个请求
Iterator (对象):如何遍历,访问一个聚合的各元素
Mediator (对象):对象间怎样交互,和谁交互
Memento (对象):一个对象中哪些私有信息存放在该对象之外,以及在什么时候进行存储
Observer (对象):多个对象依赖于另一个对象,而这些对象又如何保持一致
State (对象):对象的状态
Strategy (对象):算法
Visitor (对象):某些可作用于一个(组)对象上的操作,但不修改这些对象类
注意,设计模式不能肆意使用。通常你引入额外的间接层次获得灵活性和可变性的同时,也使得设计变得更加复杂或牺牲了一定性能。