Chapter 1

什么是设计模式

设计模式系统地命名、解释和评价了面向对象系统中一个重要的和重复出现的设计,是对用来在特定场景下解决一般设计问题的类和相互通信的对象的描述。设计模式确定了所包含的类和实例,它们的角色、协作方式以及职责分配。

设计模式的分类

按目的

创建型(creational)与对象的创建有关,结构型(structural)处理类或对象的组合,行为型(behavioral)描述类或对象怎样交互和怎样分配职责。

按范围

指定模式主要用于类还是对象。类模式处理类和子类之间的关系,该关系通过继承建立,编译时确定,为静态。对象模式处理对象间的关系,运行时可以变化,为动态。

复用机制

1.继承和组合的比较

类继承又名白箱复用(white-box reuse),因为在继承中父类的内部细节对子类可见。类继承在编译时静态定义,无法运行时改变。而且继承对子类揭示了父类的实现细节,一定程度上破坏了封装性。此外,子类和父类的依赖性紧密,一旦继承下来的实现不适合解决新的问题,就要重写/替换父类,这种依赖关系限制了灵活性和复用性。

对象组合又名黑箱复用(black-box reuse),对象的内部细节不可见,通过组装和组合对象获得新的复杂功能。这样不破坏封装性(对象只能通过接口访问),同时依赖关系较少。对象组合使类和类继承层次保持较小规模,并且这样的设计有更多的对象和更少的类,于是系统行为依赖对象间的关系,而非定义在某个类中。

原则:优先使用对象组合,而不是类继承。

2.委托

在委托方式下,接受请求的对象把操作委托给它的代理者(delegate),比如Window类中保存一个Rectangle类的实例变量,把求面积请求转发给Rectangle,而不是继承Rectangle这个类。这样做的优点是灵活,可以运行时组合对象操作,改变操作的组合方式。不足之处是动态,参数化的软件比静态软件难以理解,以及运行低效。

3.参数化类型

例子:c++中的template,这并非严格的oop技术。它允许改变类用到的类型,但与继承一样,不能在运行时改变。

关联运行时和编译时的结构

程序的代码结构在编译时就已经确定,它由继承关系固定的类组成。然而运行时结构是由快速变化的通信对象网络组成的,更多受到设计者而非编程语言的影响。

聚合(aggregation)指一个对象拥有另一个对象,比如一个对象是另一个对象的成员变量。

相识(acquaintance)指一个对象“知道”另一个对象,也被称为“关联”或者“引用”。

聚合和相识是编程者的意图决定的,它们的区别在编译时的结构中很难看出来。

导致必须重新设计的原因

  1. 显式指定一个类来创建对象:导致被特定实现约束,而非特定接口。

  2. 某一请求被指定为一个特殊操作,产生依赖。

  3. 对硬件和软件平台的依赖。

  4. 对对象表示或实现的依赖:应该对客户隐藏对象的表示、保存、定位、实现信息。

  5. 算法依赖:对象依赖某个特定算法。应该把可能发生变化的算法孤立起来。

  6. 紧耦合:互相依赖的一堆类,难以改变或删掉其中的一个。设计模式使用抽象耦合和分层技术提高系统的松散耦合性。

  7. 通过生成子类来扩充功能:可能导致类爆炸,不得不引入很多其他子类。可以用对象组合和委托技术实现新的功能。

  8. 不能方便的修改类:比如商业类库。设计模式在这种情况下提供对类进行修改的方法。

Chapter 2

文档结构

所有对象对应一个抽象基类Glyph(图元),图形元素(字符和图像)以及结构元素(行和列)都是它的子类。这样的设计做到了对文本和图形的一致对待,也没有过分强调单个元素和元素组之间的差别。
| 责任 | 操作 | | -------- | ------------------------------------- | | 表现 | virtual void Draw(Window) | | 表现 | virtual void Bounds(Rect&) | | 点击检测 | virtual bool Intersects(Const Point&) | | 结构 | virtual void Insert(Glyph, int); | | 结构 | virtual void Remove(Glyph) | | 结构 | virtual Glyph Child(int) | | 结构 | virtual Glyph* Parent() |

Draw负责让图元画出自己,Bounds指出图元占用多大空间,Intersects判断一个指定的点是否与图元相交,结构操作与图元的父图元和子图元相关。

格式化

格式化文档的算法应该被封装起来,独立于文档结构之外。

CompositionGlyph的子类,拥有Compositor对象,Compositor封装了具体的格式化算法。

支持文档物理结构的类和支持不同格式化算法的类实现了分离。

修饰用户界面

如果想添加边界或滚动条,可以选择创建Composition的子类BorderedCompositionScrollableComposition,但是组件多了有\(2^n\)排列组合,显然不可能这么做下去。可以通过对象组合的方式实现。Border应该是一个Glyph的子类,因为客户不应区分图元是否有边界而使用不同的接口。于是可以引入一个抽象基类Monoglyph,它是只有一个子图元的图元,把所有对这个组件的请求转发给它的子图元。这样一来,Border就可以继承MonoGlyph,它先调用MonoGlyphDraw函数,然后画出边界。

支持多种视感标准

如果直接用ScrollBar* sb = new MotifScrollBar来创建实例,相当于编译时定死了视感标准。此时就要用工厂ScrollBar* sb = guiFactory->CreateScrollBar().这里guiFactoryMotifFactory的实例,每种Factory都是GUIFactory抽象基类的子类。

支持多种窗口系统

我们把不同窗口系统的公共部分抽象出来加以封装成Window类,而不同窗口系统的区别则封装在WindowImp类里。Window类通过调用成员类WindowImp的对应函数实现对应功能,这样一来,不同窗口系统接口的巨大差异就被WindowImp隐藏了。这正是Bridge模式的一个例子,Window支持窗口的逻辑概念,而WindowImp描述了窗口的不同实现,两个独立演化的,分离的类层次一起工作。

用户操作

定义一个Command抽象类,它的不同子类按照各自的需求实现execute方法,所有的用户操作(如点击菜单项),只是执行了这个操作的对象的Command->Execute()方法。同时,维护一个最近执行的Command列表就能方便地实现Undo和Redo操作。

拼写检查和段字处理

1.遍历

我们需要访问子图元中的对象来分析文本,这就需要了解图元存储子图元的内部数据结构。为了保持封装,引入迭代器类Iterator.每种内部数据结构(数组、链表)都对应一个Iterator的子类。图元使用CreateIterator方法创建与其数据结构对应的迭代器,这就保证了遍历算法对用户透明。

2.分析

显然,分析工作不能在迭代器类系列中进行,因为我们希望降低耦合。如果把分析工作放在图元类里,似乎是可以的,因为对不同的图元类型,分析工作显然不同。比如,拼写检查应该考虑字符图元,颜色分割应该考虑可见的图元。但是这样一来,每增加一种新的分析,就要改变每一个图元类,并且会导致Glyph的接口越来越大,模糊它的基本接口。

因此,应该用独立对象封装分析方法。如果我们对某种分析(拼写分析)定义一个类SpellingChecker,那就可以给图元定义一个接口void CheckMe(SpellingChecker&),遍历图元时使用这个接口即可做到拼写检查。但是这样要为每种分析增加一个Checker,更严重的是,要为图元类增加一个对应的CheckMe接口。

于是引入Visitor类,它对不同类型的Glyph子类有一个VisitXXX函数。CheckMe也改为一个更加通用的名字Accept(Visitor&).添加一个新分析类型,只需要建立一个新的Visitor子类。不过一旦图元类有了一个新的子类,就需要对所有的Visitor添加一个对应的VisitXXX函数,对于文字处理应用,增加一个图元不是一个合理需求,所以这样抽象满足我们的需要。

Chapter 3

创建型模式可以分为两类,一种是生成创建对象的类的子类,对应Factory Method模式,另一种方式更多依赖于对象组合,即创建一个“工厂对象”,专门负责创建产品对象,对应Abstract Factory,BuilderPrototype模式。

Abstract Factory

提供一个接口以创建一系列相关或相互依赖的对象,而无须指定它们具体的类。

适用于:一个系统要独立于它的产品的创建、组合和表示;一个系统要由多个产品系列中的一个来配置。

对象 功能
AbstractFactory 声明一个创建抽象产品对象的操作接口
ConcreteFactory 实现创建具体产品对象的操作
AbstractProduct 为一类产品对象声明一个接口
ConcreteProduct 定义一个将被相应的具体工厂创建的产品对象
Client 仅使用由AbstractFactory和AbstractProduct类声明的接口

在运行时创建一个ConcreteFactory实例,创建具体类型的产品对象。AbstractFactory将产品对象的创建延迟到它的ConcreteFactory子类。

客户不接触到具体的产品类名,同时也易于改变产品序列,有利于产品的一致性。

然而若是要增加一个新种类的产品(即ProductC),就必须改变AbstractFactory的接口,一种改进措施是只实现一个Make接口,然后用一个标识符作为参数指明创建的是何种对象。这样做的问题是返回值会是一个相同的抽象接口,用户必须依赖强制类型转换获得具体的产品类别。

Builder

将一个复杂对象的构建和它的表示分离,使同样的构建过程可以创建不同的表示。

适用于:创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时;构造过程必须允许被构造的对象有不同的表示时。

对象 功能
Builder 为“创建产品的各部分”提供抽象接口
ConcreteBuilder 实现Builder接口,构造装配产品;定义跟踪创建的产品并提供检索接口(GetResult)
Director 构造一个使用Builder接口的对象
Product 表示被构造的复杂对象;它包含了定义其组成部件的类

用户创建Director对象并配置Builder,一旦导向器生成产品部件就通知生成器构造产品,用户通过生成器检索产品。

Builder隐藏了产品的内部表示和装配过程;分离了构造和表示过程,比如可以对RTF, SGML格式的文档各自使用一个阅读器(Director),它们可以使用同样的Builder(如ASCIIText)把该格式的文档转为ASCII字符。

Builder不需要把每个构件的生成方法定义为纯虚函数,用户可以按需重载而非全部重载。

Factory Method

定义一个用于创建对象的接口,让子类决定实例化哪一个类。(Virtual constructor)

适用于:一个类不知道它必须创建的对象的类时;一个类希望它的子类来指定它所创建的对象的时候

对象 功能
Product Factory Method所创建对象的接口
ConcreteProduct 实现Product接口
Creator 调用工厂方法以创建一个Product对象
ConcreteCreator 重定义工厂方法,返回ConcreteProduct实例

代码只处理Product接口,Factory Method使用户不再需要将与应用相关的具体类名包含在代码中。

c++的template就是一种工厂方法,而且免去了创建ConcreteCreator子类的过程,直接得到ConcreteCreator.

Abstract Factory的区别

参考资料:https://stackoverflow.com/questions/5739611/what-are-the-differences-between-abstract-factory-and-factory-design-patterns

Factory Method是方法,Abstract Factory是一个对象,一般由很多个Factory Method组成。

工厂方法用于创建某一特定产品,抽象工厂用于创建一系列相关的产品。

在使用抽象工厂模式时,一个类会把创建其他对象的职责委托给工厂对象;而使用工厂方法模式时,Creator会依赖继承序列里的ConcreteCreator来完成创建工作。

Factory Method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A {
public void doSomething() {
Foo f = makeFoo();
f.whatever();
}

protected Foo makeFoo() {
return new RegularFoo();
}
}

class B extends A {
protected Foo makeFoo() {
//subclass is overriding the factory method
//to return something different
return new SpecialFoo();
}
}

Abstract Factory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A {
private Factory factory;

public A(Factory factory) {
this.factory = factory;
}

public void doSomething() {
//The concrete class of "f" depends on the concrete class
//of the factory passed into the constructor. If you provide a
//different factory, you get a different Foo object.
Foo f = factory.makeFoo();
f.whatever();
}
}

interface Factory {
Foo makeFoo();
Bar makeBar();
Aycufcn makeAmbiguousYetCommonlyUsedFakeClassName();
}

//need to make concrete factories that implement the "Factory" interface here

Prototype

用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。

适用于:一个系统应该独立于它的产品创建、构成和表示时;当要实例化的类是在运行时指定时;避免创建一个与产品类层次平行的工厂类层次时;一个类的实例只有几种可能的状态组合,可以用原型复制比每次手工填充状态方便

对象 功能
Prototype 声明一个克隆自身的接口
ConcretePrototype 实现一个克隆自身的操作
Client 让一个原型克隆自身以创建一个新的对象

Prototype模式允许用户在运行时建立/删除原型,以将一个新的具体产品类并入系统。同时,不需要像Factory Method那样产生一个与产品类层次平行的Creator层次。

实现的时候,一般会使用一个原型管理器保存可用的原型,实现原型的动态创建和销毁。

Singleton

保证一个类仅有一个实例,提供一个访问它的全局访问点。

有些类应该保持唯一性,可以让这个类自身负责保存它的唯一实例,并提供一个访问该实例的方法。

对象 功能
Singleton 定义一个Instance操作,使用户可以访问它的唯一实例

Singleton模式可以有子类,并且用这个子类的唯一实例来初始化Instance,实现了操作的精化。

可以通过将类的构造函数设为protected做到这一点,相比于把Singleton定义为全局或者静态变量,这样做保证了只有一个实例会被创建。