《设计模式之禅》-秦小波
| 书名 | 《设计模式之禅》 |
| 作者 | 秦小波` |
| 出版日期 | 2010-03-01 |
| ISBN | 9787111295440 |
| 主要内容 | 讲述软件设计6大原则,24种设计模式及其扩展 |
设计原则
设计原则只是一种倡议,一种规范,目的是设计出更健全的软件。
设计模式不是硬性规定,应结合具体情况自由组合,灵活改变。
单一职责原则
说明
There should never be more than one reason for a class tochange.
- 有且仅有一个原因(属性、行为、协议等)引起(接口、类、方法)的变更。
- 一个方法、类、接口只做一件事。
优点
- 降低类的复杂度
- 提高可读性、可维护性
- 降低变更引起的风险,单一接口的变更不会影响其他接口
缺点
This is sometimes hard to see
- 无法确定是否分离其行为等原因,增加复杂性。
注意 - “职责”和“变化原因”可用于衡量类/接口设计优良与否的一个原因,但这是不可度量的,应适应具体问题
建议
- 接口“单一职责”
- 类“单一变化原因”
里氏替换原则
说明
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it
- 父类出现的地方完全可以用子类代替。
继承的优点
- 代码共享
- 提高代码重用性
- 提高扩展性
继承的缺点
- 侵入性,必须拥有父类的全部属性和方法
- 降低灵活性,子类多了父类的约束
- 增加耦合,父类改变则之类也必须改变
优点
- 适合升级
- 增加兼容性
要求
- 类中调用其他类时使用父类或接口(子类必须完全实现父类的方法)
- 如果子类不能完整实现父类的方法或子类某方法与父类功能不同,则该类应使用依赖、聚集、组合代替继承
- 子类可扩展父类,但尽量避免子类“个性”
- 子类可缩小父类的参数或结果
注意 - 子类“个性”后,父类较难替代子类。但子类业务独立后,代码耦合关系将晦涩难懂
依赖倒置原则
说明
High level modules should not depend upon low levelmodules. Both should depend upon abstractions. Abstractionsshould not depend upon details. Details should depend uponabstractions.
- 面向接口编程。
- 模块间的依赖通过接口/抽象发生
- 高层模块不依赖底层模块,依赖抽象
- 抽象不包括细节,细节依赖抽象
优点
- 减少耦合性
- 并行开发
- 便于测试(测试驱动开发)
- 对扩展开放,对修改关闭
依赖的传递方法
- 构造函数传递依赖对象
- 通过
setter方法传递 - 通过接口声明依赖对象
要求
- 每个类尽量都有接口/抽象类
- 变量的类型尽量用接口或抽象类
- 任何类都不应该从具体类派生(设计时不要超过两层继承,维护/扩展/修复时可不考虑该要求)
- 尽量不要覆写基类的方法
- 结合里氏替换原则
接口隔离原则
说明
客户端不应该依赖它不需要的接口
The dependency of one class to another one should de-pend on the smallest possible interface.
类使用尽可能少/小的接口,去除不要的接口/方法
(class是实例接口,interface是类接口)
要求
- 不依赖不需要的接口,对接口细化接口尽量小,最小为单一职责原则,一个接口只服务于一个子模块或业务逻辑,使类间依赖关系最小
- 接口内高内聚,接口中的公开方法尽量少
- 为不同模块定义不同接口,为特殊模块单独设置接口
- 接口设计粒度越小越灵活,但要有一个限度
注意
接口隔离原则与单一职责原则在设计接口时可能发生冲突。
- 如某N个方法,属于同一职责,应归属同一接口,但各实现并不会用到所有的N个方法,不符合接口隔离原则
接口尽量小,但不能无限小,应首先符合单一职责原则,具体情况具体对待
建议
- 一个接口只服务于一个业务逻辑/模块
- 减少
public方法 - 接口被污染后,若修改风险太大,可采用适配器模式
- 具体情况具体对待,不可照搬
迪米特法则
说明
Only talk to your immediate friends
- 一个对象只与(成员变量、方法的输入输出参数)中的类交互,具有封装性
优点
- 弱耦合
- 提高类的复用率
缺点
- 产生大量中转/跳转类
- 提高系统复杂度
要求
- 方法内尽量不引入类中不存在的对象
- 减少
public,尽量内敛 - 谨慎使用
serializable
建议
- 如果某方法放在本类,不增加类间关系,也不产生负面影响时,则放入本类
- 完全解耦是不符合实际不存在的,解耦应适度适量
开闭原则
说明
Software entities like classes, modules and functions shouldbe open for extension but closed for modifications
- 开放扩展,封闭修改
扩展
- 功能扩展时,不要修改原有方法/类/接口,应生成新类,减少对引用该类的功能的影响
修改
- 将生成的新类覆写原有方法(不修改原类)
优点
- 减少重新测试,只需添加扩展类的测试
- 提高复用性,缩小逻辑粒度
- 提高可维护性,无需修改旧代码
- 对可能引起的变化留下接口
要求
- 抽象约束
- 通过接口或抽象约束进行扩展,不允许出现接口/抽象中不存在的
public
参数类型约束 - 引用对象时尽量使用接口或抽象类
- 抽象类尽量保持稳定,减少修改
- 通过接口或抽象约束进行扩展,不允许出现接口/抽象中不存在的
- 配置参数应从外部获得
- 设定团队项目约定章程,约定优于配置
- 封装变化,预测可能的变化并封装
建议
- 封闭修改指减少修改,但不是不做任何修改,必然会有高层模块的解耦
- 可引起的变化主要为:逻辑变化、功能变化、view变化
设计模式
创建类模式
创建类模式用于创建、管理对象,或提供一个已创建的对象。
单例模式
- 一个类只能生成一个对象
方法
https://www.runoob.com/design-pattern/singleton-pattern.html
将构造函数私有化,静态初始化自己,设置方法返回本实例
- 懒汉模式:被调用时初始化本实例
- 饿汉模式:类加载时初始化本实例
- 静态内部类:设置内部类实例化对象,外部无法调用(推荐)
- 使用枚举:
enum - 悲观锁:
synchronized同步方法 - 双重校验锁:
synchronized同步代码块,并加以是否实例化的判断(推荐) - 乐观锁:
CAS(AtomicReference)
synchronized可防止对线程同时创建多个实例
优点
- 节省内存
- 可直接初始化
- 避免资源占用过多
- 避免资源占用的互斥
- 资源全局共享
缺点
- 没有接口,难以扩展
- 难以测试
- 与单一职责原则冲突
使用场景
- 唯一序列号
- 共享访问/数据(计数器等)
- 资源消耗较多的对象(数据库连接、I/O等)
- 包含大量静态常量/方法的情况(工具类等)
扩展
有上限的多例模式
- 设置可实例的最大数量
- 用列表存储每个实例的私有属性
- 列表存储所有实例
- 初始化生成最大数量的实例,设置公有方法返回指定实例
注意
- 长期不使用的实例可能被自动
GC。- 可将状态写入文件,生成对象时导入文件恢复状态
- 使用框架级容器(
Spring等)管理单例对象
原型模式
Specify the kinds of objects to create using a prototypicalinstance, and create new objects by copying this prototype
- 创建原型实例,通过拷贝该对象创建新的对象。
Java需继承自Cloneable接口并重写Object的clone方法
优点
- 性能好。克隆比生成快
- 避免构造函数的约束,克隆不会执行构造函数
缺点
- 减少了构造函数的约束
使用场景
- 初始化消耗过多资源
- 性能和安全需求(克隆不需要繁琐的数据准备和访问权限)
- 一个对象由多个修改者时,克隆减少冲突。
- 与工厂模式搭配,克隆原型对象并由工厂返回给调用者。
注意事项
- 构造函数不会被执行
- 深拷贝与浅拷贝
final不可clone,使用clone时必须取消final
工厂方法模式
Define an interface for creating an object, but let subclassesdecide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.
- 创建工厂类用于生成指定实例
如: 用户通过电视工厂购买电视,用户无需了解电视机的生产过程
组成
- 抽象工厂类
- 定义工厂的共性
- 具体工厂类
- 具体产生对象的方法,传入产品类型,返回产品对象
- 抽象产品类
- 定义产品的共性
- 具体产品类
- 工厂返回的具体产品
优点
- 封装性
- 生成对象时只需要知道生成该类的参数。
- 屏蔽产品的具体实现(由工厂负责)。
- 扩展性
- 增加产品时只需添加具体产品类型即可
- 解耦
- 符合迪米特法则、依赖倒置原则、里氏替换原则
缺点
- 需要添加工厂类,增加代码复杂度
使用场景
- 所有需要生成对象(new)的地方
- 当需要灵活多变、扩展性高的框架时
- 异构项目的管理
- 测试驱动开发框架下虚拟产品类
扩展
简单工厂模式
只有一个工厂时,无需抽象工厂类,使用静态的工厂方法即可。但扩展困难,不符合开闭原则
多个工厂类
产品较多时,可设置多个工厂,为了避免调用者与多个工厂的交流,可添加协调类封装各工厂。但难以扩展。
实现单例/多例模式
在工厂内部统计实例数量
延迟初始化
- 某对象使用完后不立即释放工厂内已生成的对象,可放入缓存容器,当再次需要创建该对象时,直接返回已生成的对象,降低对象生产/销毁的复杂性(如I/O操作,复杂交互)
- 当需要限制最大实例数量时,可返回已有对象,如数据库连接池
抽象方法模式
Provide an interface for creating families of related or de-pendent objects without specifying their concrete classes.
- 为相关联的对象提供接口,不指定具体类
- 模板方法可使用
final或protected定义
如: 车门工厂每生产两个门
优点
- 封装性。只需知道工厂,无需关心实现
- 产品族内约束为非公开状态,产品的组成结构由工厂控制,非公开的
缺点
- 难以添加新产品
- 抽象工厂添加新产品接口时,具体工厂必须修改,违反开闭原则。但添加新工厂非常容易
使用场景
- 具有多个相关联(比例等)的类/工厂组合为新产品
- 对某个逻辑(如界面)屏蔽底层(如不同的设备和操作系统)
建造者模式
Separate the construction of a complex object from its repre-sentation so that the same construction process can create differ-ent representations
将对象的构建(工厂)与他的表示(组合方式)分离,使其可以创建不同的表示方法,是工厂模式的升级。
组成
- 产品类
- 具体方法,实现模板方法和基本方法
- 抽象建造者
- 定义产品的组建行为,具体由子类实现
- 具体建造者
- 实现抽象建造者
- 导演类
- 负责安排模块构建顺序
优点
- 封装性
- 无需了解每一个具体产品的实现细节
- 易扩展
- 个建造者互相独立
应用场景
- 相同的方法,但有着不同的执行顺序
- 拥有多个组件,但组成顺序不同,结果不同
- 产品复杂,调用顺序不同
- 创建时使用到系统中其他对象时,可用于补偿简化创建过程
建议
- 使用该模式时,可考虑模板方法模式,多种工厂组合使用
扩展
- 核心是组件的执行顺序,扩展同工厂模式
创建类模式对比
单例模式
- 单例类只会创建一个对象,每次获取到的是同一个对象。
原型模式
- 新对象由原对象克隆得到,而不是
new出来的。 - 不会调用构造函数。
工厂模式
- 注重对整体的创建
- 不关心对象的组成顺序
- 适合单一性质产品(生成一样的对象)
- 一条固定的产品线
抽象工厂模式
- 多个产品线(不同产品选择不同产品线)
模板工厂模式
- 将多个产品线的通用部分设为模板
建造者模式
- 注重部件一步步构造出对象
- 适合复合产品(根据蓝图组合不同的对象)
- 可随意组合的积木
- 转型快,只需重新设计蓝图
结构类模式
结构类模式通过组合类或对象产生更大的结构
代理模式
Provide a surrogate or placeholder for another object tocontrol access to it
- 代替A访问B,对A进行访问控制,并完成预处理及善后处理。是A和B的中间层。代理也可以拥有代理。
- 代理类Proxy和B拥有相同的抽象类,每个抽象类只需要一个Proxy,将具体实现传给Proxy即可
优点
- 职责清晰
- 对访问者新增功能,不会影响访问者的原有功能。
- 高扩展性
- B发生更改,Proxy和B继承自同一接口,所有Proxy不需要更改。
- 智能化
- 动态代理——一种表单元素到对象的映射。
使用场景
- 完成访问者的非本职工作
扩展
普通代理
只能通过代理调用被访问对象,不允许直接通过访问者调用被访问者。禁止直接生成被访问对象。
强制代理
由B生成Proxy,只能通过该代理访问B的内部方法。高层模块调用时,通过B的getProxy获取生成的代理。
虚拟代理
避免被访问者较多而引起的初始化缓慢。如果代理能完成任务则不初始化被访问对象。
- 缺点
- 不能保证该被访问者已被创建,必须在所有调用该被访问对象的地方判断该对象是否已生成。
动态代理
由场景决定代理哪个接口/类,并非和B继承自同一接口,但通过反射与B绑定调用B的方法。
AOP(面向切面编程),通过预编译和运行时的动态代理实现不修改源码而为程序动态添加功能。
- OOP:面向对象的逻辑单元划分。使用名词。
- AOP:面向业务处理过程的切面,降低耦合。使用动词。如内容传递、权限处理、事物处理等,以克服单继承的缺点。
- 动态AOP:使用特定的动态代理API或
Bytecode等技术。
适配器模式
Convert the interface of a class into another interface clientsexpect. Adapter lets classes work together that couldn’t other-wise because of incompatible interfaces.
- 增加适配器,将组件A的接口转变为组件B的接口,供组件B使用
- 是一种补救模式
变压器可以将高压电转为家用电供家用电器使用
组成
- 目标
- 组件B
- 源
- 组件A
- 适配器
- 将组件A的接口转为组件B的接口
优点
- 将两个没有任何联系的组件组合运行
- 增加组件的透明性
- 对高层,组件AB是相同的
- 提高类的复用性
- 使组件可被其他组件使用
- 灵活性
使用场景
- 外部组件接口与自身接口不兼容,又难以更改/重构
注意事项
- 适配器模式是对于已服役的项目或他人项目的适配,设计阶段无需考虑。
- 项目设计一定要遵守
依赖倒置原则和里氏替换原则
扩展
对象适配器
当需要适配的对象来自多个接口时,无法通过继承适配,可将多个接口的对象作为适配参数,将继承关系变为关联/合成关系。
使用场景更多。
装饰模式
Attach additional responsibilities to an object dy-namically keeping the same interface. Decorators provide a flex-ible alternative to subclassing for extending functionality
- 动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比生成子类更为灵活。
- 装饰类的作用也就是一个特殊的代理类,真实的执行者还是被代理的角色。
组成
- 原实体接口
- 原实体
- 抽象装饰类,和原实体类一样继承自原实体接口(代理)
- 具体装饰类
实现
1 | //原实体接口 |
优点
- 装饰模式是继承的替代方案
- 继承只能修饰单个实体类,而装饰可以同时为该接口下的所有实体类添加扩展
- 可动态扩展功能
- 调用时添加扩展装饰
- 装饰类和原实体类解耦
缺点
- 多层装饰比较复杂
使用场景
- 扩展某类的功能(也可以直接用继承)
- 动态的为某个对象增加功能
- 批量为某类接口添加功能
门面模式
Provide a unified interface to a set of interfaces in a subsys-tem. Facade defines a higher-level interface that makes the sub-system easier to use.
- 模块与外界通信,必须通过一个统一的对象进行,该对象对外提供统一的高层次易于使用的接口。
组成
- 门面
- 门面了解模块的所有功能与实现,本身没有复杂的业务逻辑,并对外提供简单易用的接口。
- 模块/子系统
- 包含多个复杂的类的集合,门面对于模块是透明的。
优点
- 减少依赖,更加灵活
- 将外界与模块的强依赖转为对门面的依赖
- 提高安全性
- 外界只能通过门面访问模块,可由门面控制权限
缺点
- 不符合开闭原则
- 无法通过继承复写门面,只能修改门面,不能对修改关闭。
使用场景
- 为复杂模块/子系统提供易于使用的接口
- 模块/子系统相对独立,外界黑箱操作
- 减小开发影响范围:只在模块内开发,只影响本模块
注意事项
- 门面的拆分
- 门面功能过于庞大时,可按功能拆分为多个门面
- 权限分配
- 为不同的权限提供不同的门面
- 门面不参与模块内部逻辑
- 门面只提供访问接口,不参与业务逻辑
组合模式
Compose objects into tree structures to repre-sent part-whole hierarchies. Composite lets clientstreat individual objects and compositions of ob-jects uniformly.
- 将对象改为树形结构的组合形式,描述部分与整体的关系,使对单个对象和组合对象的使用具有一致性。
组成
- 抽象组件
- 各对象的公共方法和属性,如
getInfo。
- 各对象的公共方法和属性,如
- 叶子
- 无下属分支,及遍历的最小单位,分支的终点。
- 树枝
- 组合树枝和叶子,组成树形结构。
使用
叶子和树枝均继承自抽象组件,对用户来说叶子和树枝是同一个东西,需通过instanceof判断具体类型。
缺点
- 组合模式是对依赖倒置原则的破坏,因为无法用子类代替父类
- 叶子和树枝是实现类,而不是接口。
优点
- 调用简单
- 结构中所有节点都是抽象组件
Component,局部与整体没有任何区别,无需关心该对象是树枝还是叶子。
- 结构中所有节点都是抽象组件
- 节点增删自由
- 容易扩展,找到父节点即可。
使用场景
- 树状结构、部分-整体关系的场景,如菜单、文件夹、层次管理、DOM、JSON等。
- 可从政体中独立出部分模块或功能的场景
扩展
透明模式
将组合使用的方法放入抽象组件类
- 优点
- 面向用户的操作更加统一,叶子和树枝具有完全相同的结构。
- 缺点
- 因为叶子和树枝完全相同,但功能作用不同,在进行操作时(如
Add)需进行判断,有可能在叶子上进行Add操作。
- 因为叶子和树枝完全相同,但功能作用不同,在进行操作时(如
倒叙遍历
根节点可访问下级节点,而下级节点并不知道父节点的存在,所以需要在下级节点中增加获取/设置父节点的方法
兄弟节点排序
对子节点使用有序容器存储
享元模式
Use sharing to support large numbers of fine-grained objects efficiently.
- 使用共享对象以支持大量细粒度(性质相近)的对象
- 细粒度对象的信息可分为内部状态(可共享的不会一直改变)和外部状态(随环境改变无法共享)
组成
- 享元
- 处理与环境无关的业务与状态
- 接受外部状态
- 尽量将属性设为
final,避免无意修改
- 不可共享的享元
- 不包括外部状态,无法保证线程安全等要求
- 不会出现在享元工厂中
- 享元工厂
- 生成/获取享元对象并放入容器/池中
优点
- 减少对象的创建,降低内存占用
缺点
- 提高系统复杂度
- 需要分离内部状态和外部状态
使用场景
- 系统中生成大量相似对象
- 细粒度对象可分为内部对象与外部对象
- 缓冲池
扩展
- 线程安全
- 享元对象数量太少时,多个线程获取到同一个对象并进行修改设置外部状态,产生数据不一致。
- 数量
- 为了避免线程安全问题,享元对象应尽可能多设置
- 使用基本类型作为外部状态
- 引用类型会影响效率,还会更复杂
与对象池的区别
- 享元模式可以用在需要缓冲池的场景中,作为部分对象池的替代。
- 享元模式注重数据共享,重点是内部状态与外部状态的划分
- 对象池注重对象的复用,类似于常量。
桥梁模式
Decouple an abstraction from its imple-mentation so that the two can vary independently.
- 抽象与现实分离
组成
- 抽象角色
- 定义角色行为
- 保存一个实现的引用
- 一般为抽象类
- 实现接口
- 定义角色行为
- 具体抽象角色
- 抽象的具体实现,可以调用保存的实现,相当于对实现的封装。
- 具体实现
- 继承自实现接口
创建具体实现
I1
Implentor I = new ConcreteImplementor();
创建具体抽象角色
a,并设置保存的实现i1
Abstraction a = new RefinedAbstraction(imp);
通过
a执行方法
优点
- 抽象和实现分离
- 解决继承的缺点,使得实现不受抽象约束。扩展的抽象角色与核心的实现角色没有继承关系
- 易扩充
- 增加抽象角色和具体抽象角色即可,核心功能由实现角色去实现。
- 隐藏细节
- 抽象角色是对具体角色的一种封装
使用场景
- 继承层次过多、功能实现受继承约束等不适合使用继承时。
- 接口不稳定,可通过桥梁模式对功能修改或扩充。
- 脱离继承依赖,设计更细致的功能拆分。
注意事项
- 拆分抽象与实现
- 对变化封装,将变化封装到最小的逻辑单元中。
- 当出现多层继承时,说明对变化封装不是很好,可考虑使用桥梁模式
结构类模式对比
代理模式
- 和被代理对象继承自同一接口
- 控制被代理对象的行为是否发生,对其行为进行限制
- 预处理行为参数
- 不对被代理对象的行为做处理
- 可升级为AOP(动态代理)
- 易扩展
装饰模式
- 和被代理对象继承自同一接口
- 装饰模式是代理模式的特殊应用
- 对被代理对象的行为增强或减弱
- 不对行为参数进行预处理
- 易扩展
适配器模式
- 将其他类对象转为自己的接口
- 用于对已成熟系统的修补
- 难以扩展与替换
门面模式
- 将多个复杂对象提供简单易用的接口
- 不参与内部逻辑
- 可将大项目拆分为多个门面,形成多个模块
组合模式
- 对象为树状结构
- 叶子和树枝对外类型相同
享元模式
- 将大量相似对象的相同部分提取为一个或多个公共对象
桥梁模式
- 抽象与实现分离
- 实现不受抽象约束,没有继承关系
行为类模式
模板工厂模式
模板定义模板方法(公共方法)和基本方法(抽象方法),特定步骤由子类实现(子类实现框架或方法的微小改变)
优点
- 封装不变/通用部分,扩展可变/不同部分
- 提取公共部分代码,便于维护
- 行为由父类控制(父类只需定义/调用接口),由子类实现
缺点
- 模板工厂方法的子类只是实现(不具有扩展性),行为由模板控制,子类的执行结果影响了模板
建议
- 模板方法可加上
final,防止子类的复写 - 基本方法应尽量设计为
protected,减少对外的暴露
使用场景
- 子类拥有共有方法且方法相同时,提取公共方法
- 重要复杂的算法,可提取至模板
- 重构
扩展
- 钩子方法
- 模板执行策略/属性由子类决定
中介者模式
Define an object that encapsulateshow a set of objects interact. Mediator promotes loose couplingby keeping objects from referring to each other explicitly, and itlets you vary their interaction independently.
- 将一系列的对象交互封装到一个中介对象中,中介者使各对象非显性交互,取消多个对象规定关联关系或依赖关系,实现松耦合。
组成
- 抽象中介者
- 中介者接口
- 具体中介者
- 对个角色协调,依赖各角色
- 各角色
- 各角色都知道中介者的存在,各角色通过中介者进行通信。
优点
- 减少类间依赖
- 将一对多依赖转变为一对一(对中介)依赖
缺点
- 中介对象逻辑会很复杂
- 中介中会包含所有角色的依赖,角色越多,中介者越臃肿。
使用场景
类中出现了网状结构时转为星型结构。
全局调度管理。
- MCV中的Controller
- 信息中转站
- 控制中心
- 网关
- 中介服务
注意事项
- 中介者的滥用会使角色调用更加复杂,也可能使中介膨胀,适得其反
- 与依赖倒置原则冲突
- 各角色面对不同的业务,有具体的方法,不能在接口中严格定义各角色的方法
- 中介者一般只有一个,抽象中介者没有必要
命令模式
Encapsulate arequest as an object, thereby letting you parameterize clientswith different requests, queue or log requests, and support un-doable operations
- 将请求封装为对象,使用不同的请求执行不同的命令
组成
- 接收者Receiver
- 执行方
- 命令角色Command
- 声明需要执行的所有命令
- 调用者Invoker
- 接受执行者对象并执行命令角色接口的命令
- 请求Client
- 发起请求
实现
1 | //首先声明调用者Invoker |
优点
- 解耦
Invoker与Receiver没有依赖关系Client和Invoker低耦合
- 可扩展
Receiver容易扩展,只需实现Command接口
- 结合其他模式
- 责任链、模板方法等
缺点
- 一个
Receiver只完成一个功能的命令,命令过多时,Receiver数量膨胀
使用场景
所有认为是命令的地方
策略模式
Define a family of algorithms, encapsulate each one, andmake them interchangeable
- 将一组算法继承同一接口,隐藏具体实现,封装后统一管理
组成
- 上下文
- 封装算法和执行步骤
- 抽象策略
- 算法的抽象/接口,定义每个策略必须具有的方法和属性
- 具体策略
与代理模式的区别
- 代理模式的代理类和被代理类继承自同一接口
- 封装模式的封装类和被封装类不是同一个接口
优点
- 算法自由切换
- 避免使用多重条件判断
- 统一了算法接口,通过上下文同一调用
- 扩展性
缺点
- 策略类增多
- 每个策略都是类,复用性小,数量多
- 暴露过多
- 所有的策略类都暴露,违背迪米特法则
使用场景
- 多个类只在算法或行为上稍有不同
- 算法需要自由切换
- 需要屏蔽算法实现规则
注意事项
- 当策略数量超过4个,需考虑混合模式
扩展
- 策略枚举
- 上下文使用枚举,每个策略是枚举项(
public、final、static)。但扩展性被约束,常用于不经常变化的策略组
- 上下文使用枚举,每个策略是枚举项(
状态模式
Allow an object to alter its behavior when its internal statechanges. The object will appear to change its class.
- 改变状态时改变了行为,看起来像变了个类。
组成
- 抽象状态
- 封装环境上下文以实现状态切换
- 具体状态
- 该状态下的具体行为
- 从环境上下文将该状态转为其他状态
- 环境上下文
- 定义对外接口
- 实现状态切换功能,使得在不同状态下调用不同状态对象
- 将各状态对象声明为常量
- 具有抽象状态的所有行为,使用委托执行具体状态对应方法
优点
- 结构清晰
- 根据状态类改变行为,避免
switch和if嵌套
- 根据状态类改变行为,避免
- 遵守设计原则
- 开闭原则:增加状态只需增加子类,修改状态只修改该子类
- 单一职责原则:各状态类只负责一个状态
- 封装
- 状态切换在类内部完成
缺点
- 状态类膨胀
- 事物可拥有多个状态,每个状态对应一个子类
使用场景
- 行为随状态而不同
- 如权限、属性
- 替代条件分支嵌套
注意事项
- 行为必须受状态约束
- 状态尽量不超过5个
责任链模式
Avoid coupling the sender of a request to its receiver bygiving more than one object a chance to handle the request.Chain the receiving objects and pass the request along the chainuntil an object handles it
- 当多个对象都有机会处理某个请求时,将这些对象连成链,使请求沿着链传递,知道被某个对象接收处理为止。
- 请求只需发给第一个对象,并通过链传递给其他都有机会处理该请求的对象
组成
- 抽象处理链
- 确定处理链的调用顺序
- 具体处理者
- 处理具体执行者
实现
设置抽象处理链
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31public abstract class Handler {
private Handler nextHandler;
//每个处理者都必须对请求做出处理
public final Response handleMessage(Request request){
Response response = null;
//判断是否是自己的处理级别,可执行实现其他判断方式
//这里以等级Level为例
if(this.getHandlerLevel().equals(request.getRequestLevel())){
//执行
response = this.exec(request);
}else{
//不属于自己的处理级别
//判断是否有下一个处理者
if(this.nextHandler != null){
response = this.nextHandler.handleMessage(request);
}else{
//没有适当的处理者,业务自行处理
}
}
return response;
}
//设置下一个处理者是谁
public void setNext(Handler _handler){
this.nextHandler =_handler;
}
//每个处理者都有一个处理级别
protected abstract Level
getHandlerLevel();
//每个处理者都必须实现处理任务
protected abstract Response exec(Request request);
}其他处理者应继承该抽象类,需实现
getHandlerLevel和exec方法Request为发起请求的对象
对某个处理链进行封装,设置链的
nextHandler,并公开链第一处理者
优点
- 请求和处理分开
缺点
- 性能
- 每个请求都需要从链头开始遍历,当处理链较长时,对性能影响很大
- 调试不便
- 请求的处理是处理链的递归调用
使用场景
- 某请求可被多个处理者执行
- 为某个处理添加补救的处理情况形成处理链
注意事项
- 应避免链表过长的情况,可设置链表最大长度
观察者模式
Define a one-to-many dependency between objects so thatwhen one object changes state, all its dependents are notifiedand updated automatically.
- 在对象间设置关系,使得该对象(被观察者)改变时,通知所有依赖该对象(被观察者)的对象(观察者)进行更新。
如Event、WPF的Binding、React的State等。
组成
- 被观察者
- 管理观察者,并负责通知观察者。
- 观察者
- 观察者收到通知后,处理通知。
- 具体的被观察者
- 定义对哪些事件发送通知。
- 具体的观察者
- 各观察者的具体通知处理。
优点
- 抽象耦合
- 建立触发机制
缺点
- 开发调试更复杂
- 默认顺序执行,效率较低,可考虑采用异步
- 多级触发连锁反应
使用场景
- 关联多个场景(非组合关系)
- 事件多级触发
- 跨系统信息交互(消息队列)
注意事项
- 广播链
- 多级触发,难以调试,维护性差。
- 建议最多转发一次。
- 异步
- 异步涉及线程安全与队列。
与责任链模式的区别
- 责任链模式消息基本不修改,原样传递。
- 观察者模式通知由双方协定,可随时修改。
扩展
- Java:Observable
- C#:Event
其他
- 通知一般有两个参数(被观察者,数据),可以通过JSON或XML等远程传输
- 性能
- 多线程、异步
- 缓存(同步)
- 减少回调
发布/订阅
观察者模式也叫发布/订阅模型(Publish/Subscribe)
- 被观察者(Subject)即发布者(Provider)
- 观察者(Observer)即消费者(Consumer)
迭代器模式
Provide a way to access the elements of an aggregate objectsequentially without exposing its underlying representation
- 提供一种遍历/访问容器中各个元素的方法
- 容器指类似集合可容纳多个对象的对象
- 已集成到
collection、set等容器内,基本不需要自定义迭代器。
组成
- 迭代器
- 完成容器的遍历与增删,如
first、next、hasNext、remove等。
- 完成容器的遍历与增删,如
- 容器
- 实现类似于
iterator的方法,被迭代器访问与控制。
- 实现类似于
优点
- 方便访问容器
- 已包含在各大编程语言中,可直接使用,如
foreach、Collection等
其他
尽量不要自定义迭代器,尽量使用语言自带容器,否则可能使结构更加复杂
备忘录模式
Without violating encapsulation, capture and externalize anobject’s internal state so that the object can be restored to thisstate later.
- 不破坏封装性的前提下,保存该对象的状态,并可在需要时将该对象恢复到已保存的状态。
组成
- 保存的目标对象
- 负责创建备忘录,将备份数据写入备忘录
- 从备忘录获取保存的数据。
- 备忘录
- 存储、获取备份的数据
- 备忘录管理员
- 存储、获取备忘录
使用方法
- 创建目标对象
- 创建备忘录管理员
- 备份:目标对象创建备忘录(目标对象向备忘录设置备份数据)并交由备忘录管理员
- 恢复:目标对象恢复由备忘录管理员管理的备忘录(目标对象从备忘录获取并恢复备份数据)
使用场景
- 需保存、恢复数据或状态的场景
- 提供可回滚、撤销、返回的操作
- 监控副本
- 监控系统主业务的副本,而不影响主业务
- 事务管理
注意事项
- 生命周期
- 创建备忘录后立即使用,不用后立即删除
- 性能
- 不要频繁建立备忘录(设置备份会消耗性能)
扩展
- Clone
- 目标对象克隆自身实现备忘录功能,不再需要备忘录管理员
- 但涉及深拷贝浅拷贝问题。
- 检查点/多备
- 由备忘录管理员管理多个备忘录,并设置对应的KEY
- 检查点过多时,可能内存溢出
- 权限
- 备份应只能由目标对象获取
- 可将目标对象的备份方法设为内部类,并通过接口访问。
访问者模式
Represent anoperation to be performed on the elementsof an object structure. Visitor lets you define a new operationwithout changing the classes of the elements on which it oper-ates.
- 封装对某些元素的操作,不改变原有对象数据结构而定义新功能。
组成
- 访问者
- 可以访问哪些元素
- 元素
- 允许哪类访问者访问,并将自己传给访问者
- 生产者
- 生产元素。一般为
List、Map等容器。
- 生产元素。一般为
优点
- 单一职责原则
- 元素只提供数据,由访问者处理数据
- 扩展
- 可随时添加访问者,对数据进行不同处理
- 灵活
缺点
- 元素对访问者公开了内部细节
- 元素修改麻烦
- 修改元素需要修改对应的访问者
- 违反依赖倒置原则
- 访问者依赖具体元素,而不是接口
使用场景
- 对象内包含很多种数据,这些数据来自不同接口,想对这些数据进行具体类的操作
- 对对象内的数据进行很多不同且不相关的操作,将这些操作交给访问者
- 对迭代器模式的扩展
- 拦截器
- 大规模重构
扩展
- 使用访问者模式对数据进行统计操作
- 添加多个访问者
解释器模式
Given a lan-guage, define a representation for its grammar along with an in-terpreter that uses the representation to interpret sentences inthe language.
- 定义一种语法及其解释器,使用解释器解释/执行这句话
组成
- 解释器
- 终结符
- 通常只有1个终结符解释器,处理各元素及类型转换
- 非终结符
- 每条规则对应1个非终结符解释器,如
+、-、×、÷。 - 非终结符解释器将递归调用附近表达式直至终结符表达式
- 每条规则对应1个非终结符解释器,如
- 终结符
- 环境上下文
将语句转为语法树
优点
- 易扩展/修改
- 只需修改/增加相应的非终结符表达式
缺点
- 非终结符表达式类碰撞
- 语法规则复杂时,每种语法都需要一个非终结符表达式
- 递归
- 调试复杂
- 效率
- 大量使用循环和递归,对于复杂、冗长的语法可能效率低
使用场景
- 重复元素
- 各场景基本元素相同,只是不同的组合,增加非终结符表达式
- 简单语法的解释
- 较复杂的可以使用
SQL等专业工具
- 较复杂的可以使用
- 商业环境
注意事项
- 不要在重要模块中使用解释器模式,难以维护,可以使用
shell、python等脚本语言替代解释器模型
扩展
- 对于特定类型发生频率足够高时,可考虑使用解释器模式。
- 使用常见的语法格式和解析工具(
MESP、SQL等)
行为类模式对比
模板工厂模式
- 将多个产品线的通用部分设为模板
中介者模式
命令模式
- 请求与执行分离
- 请求可排队执行
策略模式
- 封装互相独立且可替换的业务
- 减小内部业务改变对外界的影响
- 使用哪种业务由调用者决定
- 算法替换设计简单
状态模式
- 封装不同状态,使状态改变时行为随之改变。
- 可限制状态的改变,各状态间有转换关联。
- 状态变化复杂
