1. 设计模式概述
1.1. 定义
对在特定场景下解决设计问题的类或对象的描述.
1.2. 特性
-
代码重用性: 相同功能的代码不用多次编写.
-
可读性: 代码规范性, 便于其他程序员的阅读和理解.
-
可扩展性: 便于增加新的功能.
-
可靠性: 当增加新的功能后, 对原有的功能没有影响.
-
高内聚低耦合: 每个功能模块内部关系紧密, 但是模块之间不会相互影响.
1.3. 要素
-
名称
-
使用场景
-
模式的组成和设计
-
效果(优缺点)
1.4. 原则
1.4.1. 单一职责原则
一个类/方法应该只负责一项职责. 降低类/方法的复杂度, 从而降低改动造成的风险.
1.4.2. 接口隔离原则
使用方不应该依赖它不需要的接口. 一个类对另一个类的依赖应该建立在最小的接口上. 可以将一个接口分成几个小接口.
1.4.3. 依赖倒转原则
底层模块尽量要有抽象类和接口. 而高层模块不应该直接依赖底层模块的具体实现类, 而应该依赖其抽象. 尽量面向接口编程.
1.4.4. 里氏替换原则
所有引用父类的地方必须能够透明地使用其子类的对象. 在子类中尽量不要重写父类的方法, 或者使用聚合/组合/依赖来解决问题.
1.4.5. 开闭原则
对功能提供者开放扩展, 对使用方关闭修改. 尽量在原有类上新增功能.
1.4.6. 迪米特原则
尽量减少类之间的交互. 一个类对自己依赖的类知道的越少越好. 尽量避免在局部变量中直接使用其他类.
1.4.7. 合成复用原则
尽量使用聚合/组合/依赖的方式使用与自己不相关的类的功能, 而不是使用继承.
2. UML
用来描述系统中的类/对象本身的组成和类和类之间的各种静态关系.
2.1. 类之间的关系
-
依赖: 一个类用到了另外一个类, 则两个类之间存在依赖关系.
虚线+箭头
(成员属性/返回类型/方法参数类型/局部变量类型) -
关联: 1对1/1对多/多对多的关系.
实线+箭头
-
泛化: 继承一个类.
实线+空心箭头
-
实现: 实现一个接口.
虚线+空心箭头
-
聚合: 表示整体和部分的关系, 整体和部分可以分别运行. (通过set方法设置依赖类的对象)
实线+空心菱形
-
组合: 表示整体和部分的关系, 整体和部分不可以分开, (实例化时就初始化依赖的类)
实线+实心菱形
3. 设计模式分类
-
创建型: 将对象的部分创建工作延迟到具体实现类.
-
结构型: 描述了对象的依赖方法.
-
行为型: 描述一组对象协作来完成单个对象无法完成的任务.
4. 创建型模式
4.1. Singleton模式
采取一定的方法保证整个软件系统中对某个类只能存在一个对象实例, 并且该类只提供一个取得其对象实例的方法. 内部提供一个静态的工厂方法, 获取单个实例.
4.1.1. 使用场景
-
系统中该类不需要多个实例.
-
重量级对象.
-
工具类.
4.1.2. 结构
4.1.3. 优点
-
节约系统资源.
-
限制了对实例的访问.
4.1.4. 缺点
-
扩展困难
4.1.5. 单例模式写法
private static class S1 {
private static final S1 INSTANCE = new S1();
public static S1 getInstance() {
return INSTANCE;
}
}
private static class S2 {
private static final S2 INSTANCE;
static {
INSTANCE = new S2();
}
public static S2 getInstance() {
return INSTANCE;
}
}
private static class S3 {
private static volatile S3 INSTANCE;
public synchronized static S3 getInstance() {
if (INSTANCE == null) {
INSTANCE = new S3();
}
return INSTANCE;
}
}
private static class S4 {
private static volatile S4 INSTANCE;
public static S4 getInstance() {
if (INSTANCE == null) {
synchronized (S4.class) {
if (INSTANCE == null) {
INSTANCE = new S4();
}
}
}
return INSTANCE;
}
}
private static class S5 {
public static S5 getInstance() {
return S5Holder.INSTANCE;
}
private static class S5Holder {
private static final S5 INSTANCE = new S5();
}
}
private enum S6 {
INSTANCE;
public void doStuff() {
// NO-OP
}
}
4.2. 简单工厂模式
定义一个工厂类, 根据不同的参数返回不同的类型. 被创建的对象类型通常具有共同的父类.
4.2.1. 使用场景
调用方知道创建对象需要的参数, 不关心对象的创建过程.
4.2.2. 结构
4.2.3. 优点
-
将对象的创建和对象的使用分离开, 客户端无需知道具体的创建逻辑.
4.2.4. 缺点
-
工厂类职责过重, 如果工厂类出现问题, 就会影响整个系统.
-
系统扩展困难, 一旦添加新的产品就要修改工厂逻辑, 违反了开闭原则.
4.3. Factory Method模式
定义一个创建对象的接口, 让子类决定将哪个类实例化.
4.3.1. 使用场景
调用方不知道它需要的对象的类型.
4.3.2. 结构
4.3.3. 优点
-
客户端只需要关心创建产品需要的工厂, 无需关心创建细节.
4.3.4. 缺点
-
系统文件个数增加
4.4. Abstract Factory模式
4.4.1. 使用场景
需要多种类型的多种对象, 且每次只使用同一类型的对象.
4.4.2. 结构
4.4.3. 优点
-
增加一个产品族的时候只需要增加一个工厂类就行了.
4.4.4. 缺点
-
增加一个产品需要修改所有的工厂类.
4.5. 原型模式
使用原型实例指定创建对象的种类, 并且通过拷贝这些原型创建新的对象.
4.5.1. 使用场景
创建大量内容相同的对象.
4.5.2. 结构
4.5.3. 优点
-
简化对象的创建过程, 提高新对象的创建效率
4.5.4. 缺点
-
为了deep clone, 对象的每一个属性都必须实现deep clone.
4.6. Builder模式
提供Builder类组装对象
4.6.1. 使用场景
组装的类属性很多, 创建过程复杂, 可以引入builder简化客户端调用.
4.6.2. 组成
4.6.3. 优点
-
隔离复杂对象的创建和使用, 并使得相同的创建过程可以创建不同的对象.
4.6.4. 缺点
-
如果产品内部变化复杂, 则会导致需要定义很多具体的建造者来实现这些变化, 导致系统臃肿.
5. 结构型模式
5.2. Bridge模式
5.2.1. 使用场景
可以将复杂系统按照抽象和实现两个维度拆分.
5.2.2. 结构
5.2.3. 优点
-
能灵活扩展一个维度而不影响其他维度
5.2.4. 缺点
-
难以分离维度
5.3. Decorator模式
5.3.1. 使用场景
在不改变原有类的基础上, 扩展现有的功能.
5.3.2. 结构
5.3.3. 优点
-
减少了子类的个数, 扩展性提高
-
可以通过不同的装饰创造出不同行为的组合.
5.3.4. 缺点
-
产生较多的对象
5.4. Composite模式
定义一个结构类, 包含所有的组件方法, 组合所有子组件和容器组件.
5.4.1. 使用场景
希望忽略整体和部分的差异, 让客户端一致地对待他们
5.4.2. 结构
5.4.3. 优点
-
可以清楚地定义分层次的复杂对象.
-
方便增加组件
-
客户端可以无需关心子组件的层次结构, 统一处理.
5.5. Facade模式
为子系统提供统一的入口
5.5.1. 使用场景
客户端程序与子系统有很大的关联性. 但不需要关系子系统的内部实现细节.
5.5.2. 结构
5.5.3. 优点
-
降低客户端与子系统的耦合度.
-
一个子系统的修改与其他系统没有影响
5.6. FlyWeight模式
实现多个细粒度对象的复用. 使用工厂获取对象.
5.6.1. 使用场景
对象需要的数量较多但对象内部状态统一.
-
内部状态是存储在享元对象内部并且不会随环境改变而改变的状态, 因此内部状态可以共享.
-
外部状态是随环境改变而改变的、不可以共享的状态. 享元对象的外部状态必须由客户端保存, 并在享元对象被创建之后, 在需要使用的时候再传入到享元对象内部。一个外部状态与另一个外部状态之间是相互独立的.
5.6.2. 结构
5.6.3. 优点
-
可以极大减少内存中对象的数量.
-
内外部状态独立.
5.6.4. 缺点
-
分离内外部状态, 使系统更复杂.
5.7. Proxy模式
给某一个对象提供一个代理对象, 由他控制对原对象的访问.
5.7.1. 使用场景
无法直接访问某个对象, 或者访问困难.
5.7.2. 结构
5.7.3. 优点
-
协调调用者和被调用者, 降低系统耦合度.
6. 行为型模式
6.1. Template Method模式
6.1.1. 使用场景
描述算法大致的流程, 具体细节交给子类去实现.
6.1.2. 结构
6.1.3. 优点
-
把公用的行为搬到父类去, 去除子类的重复代码.
6.1.4. 缺点
-
子类执行的结果影响到了父类.
6.2. Command模式
6.2.1. 使用场景
向对象发送请求, 但不需要知道谁接收并处理请求.
6.2.2. 结构
6.2.3. 优点
-
调用者和接收者解耦.
6.2.4. 缺点
-
系统需要大量的命令类.
6.3. Visitor模式
6.3.1. 使用场景
封装作用于各种对象的不同操作. 在被访问的类里加一个对外提供接待访问者的方法.
6.3.2. 结构
6.3.3. 优点
-
将某一类型的行为统一管理.
6.3.4. 缺点
-
如果新增一个Element, 那么所有的Visitor都要改动.
6.4. Iterator模式
6.4.1. 使用场景
用一致的接口遍历元素, 而不需要数组元素的底层存储方式.
6.4.2. 结构
6.4.3. 优点
-
调用方不需要关系聚合类型的数据组织形式.
6.4.4. 缺点
-
每个聚合对象都需要一个迭代器.
6.5. Observer模式
6.5.1. 使用场景
一个对象的变更需要通知其他对象.
6.5.2. 结构
6.5.3. 优点
-
将表示层和逻辑层分离
6.5.4. 缺点
-
观察者只能知道数据发生变化, 而不知道变化的来源和过程.
6.6. Mediator模式
6.6.1. 使用场景
-
通过一个中间类来封装多个类的行为.
6.6.2. 结构
6.6.3. 优点
-
简化对象之间的交互.
-
简化行为的实现.
6.6.4. 缺点
-
中介者逻辑复杂.
6.7. Memento模式
6.7.1. 使用场景
在不破坏封装性的前提下, 需要暂时在对象之外保存它的状态. 等待恢复到这个状态.
6.7.2. 结构
6.7.3. 优点
-
调用方不需要关心状态保存的细节.
6.7.4. 缺点
-
如果类的成员变量过多, 则会占用较多的资源.
6.8. Interpreter模式
6.8.1. 使用场景
解析一定语义的表达式得到结果.
6.8.2. 结构
6.8.3. 优点
-
易于改变和扩展文法.
-
增加新的解释表达式比较容易.
6.8.4. 缺点
-
对于复杂文法难以维护.
6.9. State模式
6.9.1. 使用场景
解决对象在不同状态转换时, 需要对外输出不同行为的问题.
6.9.2. 结构
6.9.3. 优点
-
代码可读性强.
-
方便维护, 减少if-else.
-
符合开闭原则, 容易增删状态.
6.9.4. 缺点
-
如果状态较多, 则会产生很多状态类.
6.10. Strategy模式
6.10.1. 使用场景
定义一些算法, 封装起来, 这些算法可以互相替换.
6.10.2. 结构
6.10.3. 优点
-
将责任和具体算法分离, 便于替换.
6.10.4. 缺点
-
调用方需要知道所有具体的策略, 以便使用合适的算法.
6.11. Chain of Responsibility模式
6.11.1. 使用场景
系统中有多个对象处理请求, 所有对象构成链式结构.
6.11.2. 结构
6.11.3. 优点
-
职责链中的对象无需关心链的结构.
-
新增或者删除handler的时候只需要修改客户端类.
6.11.4. 缺点
-
客户端责任较重.