第一章 设计模式入门

使用模式最好的方式是:”把模式装进脑子里,然后在你的设计和已有的应用中,寻找何处可以使用它们。”

通过一个给 Duck 类型添加 fly 功能的实例,一步步地提出解决方案。

先是使用继承,但是继承并不能很好地解决问题,因为鸭子的行为在子类里不断地改变,并且让所有的子类都有这些行为是不恰当的。

接着又提出使用接口,但是 Java 接口不具有实现代码,所以继承接口无法达到代码的复用。这意味着: 无论何时你需要修改某个行为,你必须得往下追踪并在每一个定义此行为的类中修改它,一不小心,可能会造成新的错误!

进而提出了第一个设计原则:

设计原则一:封装变化。找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。

这样的概念很简单,几乎是每个设计模式背后的精神所在。所有的模式都提供了一套方法让“系统中的某部分改变不会影响其他部分”。

取出易于变化的部分

设计原则二:针对接口编程,而不是针对实现编程。

接口和对应的类

关键在于,鸭子现在会将飞行和呱呱叫的动作“委托”(delegate)别人处理,而不是使用定义在 Duck 类(或子类)内的呱呱叫和飞行方法。

整合鸭子的行为

设计原则三:多用组合,少用继承。

第二章 观察者模式

观察者模式

定义:观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。

类图如下:

类图

设计原则四:为了交互对象之间的松耦合设计而努力。

松耦合的设计之所以能让我们建立有弹性的 OO 系统,能够应对变化,是因为对象之间的互相依赖降到了最低。

第三章 装饰者模式

设计原则五:类应该对扩展开放,对修改关闭。

怎么理解呢?允许系统在不修改代码的情况下,进行功能扩展。想想观察者模式(在第 2 章)……通过加入新的观察者,我们可以在任何时候扩展 Subject(主题),而且不需向主题中添加代码。

要让 OO 设计同时具备开放性和关闭性,又不修改现有的代码,需要花费许多时间和努力。一般来说,我们实在没有闲工夫把设计的每个部分都这么设计(而且,就算做得到, 也可能只是一种浪费)。遵循开放-关闭原则,通常会引入新的抽象层次,增加代码的复杂度。你需要把注意力集中在设计中最有可能改变的地方,然后应用开放-关闭原则。

定义:装饰者模式动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。

类图:

类图

第四章 工厂模式

简单工厂

简单工厂并不是一个设计模式,它是一个编程习惯。

简单工厂类图

工厂方法

工厂方法用来处理对象的创建,并将这样的行为封装在子类中。这样,客户程序中关于超类的代码和子类对象创建代码解耦了。

通过让子类决定该创建的对象是什么,来达到将对象的创建过程封装的目的。

工厂方法 —— pizza

工厂方法类图 —— pizza

创建者类和产品类可以放平:

平行的类层级

定义:工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。

注意,上面的“决定”并不是指模式允许子类本身在运行时做决定,而是指在编写创建者类时,不需要知道实际创建的产品是哪一个。选择了使用哪个子类,自然就决定了实际创建的产品是什么。

工厂方法类图

简单工厂和工厂方法的工区别

设计原则:依赖倒置原则。要依赖抽象,不要依赖具体类。

不能让高层组件依赖低层组件,而且,不管高层或低层组件,“两者”都应该依赖于抽象。倒置是指和一般 OO 设计的思考方式完全相反。低层组件现在依赖高层的抽象,而高层组件现在也在依赖相同的抽象。

抽象工厂

定义:抽象工厂模式提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。

抽象工厂类图

抽象工厂类图 —— pizza

第五章 单件模式

也就是我们常说的单例模式,有些对象只能有一个实例,例如线程池、数据库连接等。全局变量也可以做到这一点,但全局变量必须一开始就要创建好对象,如果一直没用到,就形成浪费了。

定义:单件模式确保一个类只有一个实例,并提供一个全局访问点。

应对多线程,有三种应对方法:

  1. 直接加同步原语,在 Java 中是 synchronized。很简单,可能会有一些性能问题。
  2. 使用饿汉式,启动时即创建。
  3. 用双重加锁检查,即 double-checked locking。

用 Go 来实现,非常简单:

package singleton

import "sync"

//Singleton 是单例模式类
type Singleton struct{}

var singleton *Singleton
var once sync.Once

//GetInstance 用于获取单例模式对象
func GetInstance() *Singleton {
	once.Do(func() {
		singleton = &Singleton{}
	})

	return singleton
}

第六章 命令模式

命令模式:将“请求”封装成对象,以便使用不同的请求,队列或者日志来参数化其他对象。命令模式也支持可撤销操作。

命令模式本质是把某个对象的方法调用封装到对象中,方便传递、存储、调用。

第七章 适配器模式和外观模式

这两个模式比较简单,工作中也用得比较多,只是叫不出名字。

适配器模式:将一个类的接口,转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作无间。

有对象适配器和类适配器两类,前者使用组合,后者使用多重继承。后者的好处是不用实现目标类的所有方法。

对象适配器、类适配器

外观模式:提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用。

两者的区别:只能从意图上来区别。适配器的意图是将接口转换成不同接口;而外观模式的意图是简化接口。另外需要注意的是:虽然大多数教科书所采用的例子中适配器只适配一个类,但是实际上适配器模式也可以将多个类接口转换成客户希望的一个接口。

设计原则七:最少知识原则。只和你的密友谈话。

外观模式

第八章 模板方法模式

很多模式之前或多或少地用过,像这一章的模板方法模式,去年在重构热力图的时候就用到过。当然,那时并不知道有这么个名字,只是很自然地一个想法。所有很多东西还是最后要上升到理论层次,就感觉不一样了。

定义:模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

因为 Golang 不提供继承机制,需要使用匿名组合模拟实现继承。

此处需要注意:因为父类需要调用子类方法,所以子类需要匿名组合父类的同时,父类需要持有子类的引用。

第九章 迭代器与组合模式

定义:迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。

定义:组合模式允许你将对象组合成树形结构来表现“整体/部分”层次结构。组合能让客户以一致的方式处理个别对象以及对象组合。

组合模式常用于树状结构,用于统一叶子节点和树节点的访问,并且可以用于应用某一操作到所有子节点。

参考资料

【读书会】 https://shimo.im/sheets/2nVYYYYNtjo82fTg/9YqrE

【设计模式中文网站】 https://refactoringguru.cn/design-patterns

【Go 设计模式】 https://github.com/senghoo/golang-design-pattern