1. 理论
程序员的时间与脑力是有限的,但系统复杂度的增长是无限的。
设计模式可以将系统拆分成【稳定的部分】与【变化的部分】以应对这种变化:
- 对于稳定的部分,设计模式可以将稳定的部分进行抽象,将其定义为统一的模型与流程
- 对于变化的部分,设计模式可以将变化的部分进行封装,降低变化部分变化变化后带来的修改成本
1. 开放封闭原则 (OCP,Open For Extension, Closed For Modification Principle)
- 类的改动是通过增加代码进行的,而不是修改源代码。
- 不要让使用者能够修改源代码,让使用者扩展功能。
2. 单一职责原则 (SRP,Single Responsibility Principle)
- 类的职责要单一,对外只提供一种功能,而引起类变化的原因都应该只有一个。
- 如果职责过多,就多拆几个类。
3. 依赖倒置原则 (DIP,Dependence Inversion Principle)
- 依赖于抽象(接口),不要依赖具体的实现(类),也就是针对接口编程。
- 不要让具体实现的修改影响到所有的依赖方。
4. 接口隔离原则 (ISP,Interface Segegation Principle)
- 不应该强迫客户的程序依赖他们不需要的接口方法。一个接口应该只提供一种对外功能,不应该把所有操作都封装到一个接口中去。
- 同样,如果功能过多,就多拆几个接口。
5. 里氏替换原则 (LSP, Liskov Substitution Principle)
- 任何抽象类出现的地方都可以用他的实现类进行替换。
6. 优先使用组合而不是继承原则(CARP,Composite/Aggregate Reuse Principle)
- 如果使用继承,会导致父类的任何变换都可能影响到子类的行为;如果使用对象组合,就降低了这种依赖关系。
- 例如使用注入的Bean来组装出功能,而非继承功能。
7. 迪米特法则(LOD,Law of Demeter) 最少知道法则
- 一个对象应当对其他对象尽可能少的了解,从而降低各个对象之间的耦合,提高系统的可维护性。
- 例如在一个程序中,各个模块之间相互调用时,通常会提供一个统一的接口来实现。这样其他模块不需要了解另外一个模块的内部实现细节,这样当一个模块内部的实现发生改变时,不会影响其他模块的使用。(黑盒原理)
设计模式的分类
总体来说设计模式分为三大类:
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
2. 结构型模式——更优雅的声明和构建对象
- 适配器模式:将一个物件的界面’转接’成当事人预期的样子。
- 翻新界面模式: 同时使用多个类别的界面的适配器。
- 适配器导管:因除错目的而使用多个适配器。
- 聚集模式:一种组合模式的版本,包含用于聚集子成员的成员函式。
- 桥接模式:将一个抽象与实现解耦,以便两者可以独立的变化。
- 墓碑模式:一种中介的查询物件,包含物件的实际位址。
- 组合模式:树状结构的物件,每个物件有相同的界面
- 修饰模式:对一个执行的类别,若使用继承方式加上新功能可能会新类别的数量呈指数型地增加,可使用此模式来解决。
- 扩充模式:亦即框架,将复杂的程式码隐藏在简单的界面后
- 外观模式:对于已有的界面建立一个简化的界面以简化使用共通任务。
- 享元模式:通过共享以便有效的支持大量小颗粒对象。
- 代理模式:为其他对象提供一个代理以控制对这个对象的访问。
2.1 适配器模式——将一个类的接口适配成用户所期待的类
将一个类的接口适配成用户所期待的类:一个适配允许通常因为接口不兼容而不能在一起工作的类工作在一起,做法是将类自己的接口包裹在一个已存在的类中。
应用举例
- DDD里边的防腐层(主动隔离外部接口)
- 依赖大量同一地位但接口格式不统一的下游服务,通过适配统一供上层使用(主动隔离外部接口)
- 项目迁移,适配老接口(被迫适配变化的接口)
实例详解
- 问题:业务数据来源于多个有不同接口定义的服务,如何屏蔽各个服务之间的差异?
- 不变的因素:下游服务的使用场景和提取到的信息类型。
- 变化的因素:下游服务的接口定义。
- 方案:将使用场景做抽象,将接口定义封装。
2.2 代理模式——创建一个代理对象劫持对目标对象的调用,为目标访问增加一层控制和拦截,将控制逻辑和主逻辑隔离
应用举例
- 系统权限控制 SSO
- mapi
- spring AOP
实例详解
2.3 装饰器模式——在一个操作的基础上,添加另一个操作 做增强
动态的为现有的对象添加职责或行为。
应用举例
JavaIO类库
数据双写
实例详解
- 问题:如何在一个必经操作的基础上,随时附加另一个或多个可选操作?
- 不变的因素:基础的操作。
- 变化的因素:增强的额外功能或者业务动作。
- 方案:将增强功能作为可以嵌套的装饰器。
代理模式和装饰器模式区别
目的性不同,代理是控制,装饰是附加
实现上有点区别,代理一般就一层,装饰器一般都会嵌套
代理模式不要求同源,装饰器模式要求同源
客户端是不能访问被代理类的,由代理类决定被代理对象是什么,而装饰对象是可以被访问的,客户端决定装饰哪个对象
2.4 组合模式——将对象组合成树形结构以表示“部分-整体”的层次结构
应用场景
需要表示多个对象,且对象具有相互嵌套的关系,嵌套的层级深度是不定的
不管哪一层级,所有对象包含的方法都类似
需要能通过递归遍历所有对象
应用举例
多级菜单、文件系统、组织架构、实例详解
基本上就是树结构在设计模式的体现,以组织架构为例
2.5 门面模式——供一个统一的接口去访问多个子系统的多个不同的接口
需要注意的是,门面模式并没有解决耦合,实际上是把耦合封装到一起了。
- 某一服务或流程内部逻辑特别复杂,且外部不需要知道实现细节
- 客户端和服务端耦合严重
- 分层系统架构设计,每层逻辑会对底层逻辑封装
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
3. 创建型模式——过程对象的生产与业务过程解耦
工厂模式
简单工厂 -> 工厂方法 -> 抽象工厂,是从简单到复杂的演进,目标都是解耦对象创建的过程,对创建者屏蔽内部细节。
多个产品、一个工厂生产 -> 多个产品、多个工厂一对一生产 -> 多个产品、多个工厂一对多生产
需要创建的对象种类越多、越复杂,就推动工厂方法向下一个阶段演进,但是其最终目标与思想是不变的。
3.1 简单工厂——创建单一对象的工厂,完全对外屏蔽细节
包含角色
- 产品接口: 定义了产品的共用资源例如方法,提供给子类继承使用。
- 产品类: 继承产品接口,实现接口的方法,从而产生各种各类的产品。
- 工厂类: 定义了创建产品的方法,负责创建产品对象。
3.2 工厂方法——创建同一类对象的工厂,由同一个工厂接口派生(产品和创建者一一对应)
包含角色
- 产品接口: 定义了产品的共用资源例如方法,提供给子类继承使用,强制子类实现。
- 产品类: 继承产品接口,实现接口的方法,也可以覆盖接口的方法,从而产生各种各类的产品。
- 工厂接口: 定义了创建产品的方法,具体创建者必须继承该类,实现工厂方法。
- 工厂实现类: 继承工厂接口,实现工厂方法,负责创建产品对象。
适应场景
- 客户端不用关心创建过程
- 需要创建的对象的类型较多,每一类对象的创建比较复杂
- 对象的类型还存在较大的扩展可能性
3.3 抽象工厂——每个工厂可生产多个产品
创建同一组对象的工厂,用于创建多个但在逻辑上有关联的对象
包含角色
- 产品族接口:产品家族的父类,由此可以衍生很多子产品。
- 具体产品:继承抽象产品接口,由工厂方法直接创建。
- 工厂接口: 定义了创建产品的方法,具体创建者必须继承该类,实现工厂方法。
- 工厂实现类: 继承工厂接口,实现工厂方法,负责创建产品对象。
简单工厂实现简单,内部封装多个对象的创建逻辑,所以容易造成工厂类过于庞大
工厂方法符合开闭原则,缺点是客户端要知道所需的工厂
如果要创建的对象是一个系列的、相互有关联关系,比如夜间模式的按钮、选框、文本,可以用抽象工厂
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
4. 行为模式——更清晰的表达和维护模型之间的关系
-责任链模式:处理命令物件或将之传到下一个可以处理的物件。
- 命令模式:命令物件将动作及参数封装起来。
- “具现化堆叠”:使用堆叠将递回函式转成重复执行。
- 解释器模式:实作特制化的编程语言以解决一系列特殊的问题。
- 迭代器模式:迭代器用于存取包含器中元素而不用透露底层实作的方式。
- 调停者模式:对子系统中的界面集面提供一个统一的界面。
- 备忘录模式:使一个物件还原到前一个状态的能力(rollback)。
- 空物件模式:扮演预设物件的角色。
- 观察者模式:亦即发行/订阅或事件聆听者。物件注册去聆听由另一个物作所引发的事件。
- 弱参照模式:将观察者与可观察间的藕合程度。
- 协议栈:通讯是由许多封装成阶层式的层所处理。
- 状态模式:在执行可以部分改变物件的一种方法。
- 策略模式:在执行时依需求而选择算法。
- 规格模式:以布林值的方式来重组事务逻辑。
- 模板方法模式:描述一个程式的骨架。
- 访问者模式: 一种从物件中取出算法的方式。
- 单服务访问者模式:最佳化访问者。使用一次后即行删除。
- 阶层式访问者模式:提供一种方式可以拜访阶层式数据结构如树中的每一个节点 。
- 排程任务模式:在特定区时或时间点执行排程任务(用于即时计算)。
4.1 策略模式——将一组算法中每一个算法封装到具有共同接口的的类中,从而使他们可以相互替换
应用场景
- 多套同源的算法和逻辑
- 算法的增加速度和迭代速度快
- 同时存在多种逻辑
应用举例
- 配置类
实例详解
- 问题:如何针对不同的用户采用不同的营销策略,尤其在营销策略不断变化迭代迅速的情况下?
- 不变的因素:用户及用户与营销策略之间的交互逻辑。
- 变化的因素:不断变化的营销策略。
- 方案:将策略作为封装
4.2 访问者模式——供一个统一的接口去访问多个子系统的多个不同的接口
访问者模式在实际开发中使用的非常少,因为它比较难以实现并且应用该模式肯能会导致代码的可读性变差,可维护性变差,在没有特别必要的情况下,不建议使用访问者模式.
应用场景
- 一个服务拥有⼤量的数据内容(字段)
- 大量的服务需要查询这些数据内容
- 这些查询服务每次查询的数据内容差异很大
应用举例
查询接口
实例详解
- 问题:对于底层订单信息存在多样化的查询需求,是开发多套查询接口,还是提供一个大而全的接口把数据全部返回?
- 不变的因素:查询内容的来源。
- 变化的因素:查询的请求。
- 方案:将查询的请求做封装,通过访问者访问
4.3 观察者模式
定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们自动更新自己。
应用场景
- 某⼀个具体的执行步骤之后,有多少后续下游步骤不不确定或者数量过多
- 这些后续步骤的执⾏行时间、时⻓以及结果,不改变已经执⾏的被观察步骤的执⾏结果和语义
- 这些后续步骤之间,没有结果相关性
应用举例
Spring中的发布订阅模式
在实际项目中就是用到了ApplicationContext的订阅模式机制,通过publishEvent和Listener的onApplicationEvent()做监听,并在onApplicationEvent()上做双删的切面扩展。
实例详解
- 问题:在订单状态变化后,进行一批相关操作(订单添加->更新索引;订单退款->更新索引+发送短信)
- 不变的因素:订单状态变化的发生结果不会更改,其步骤与语义也不会变化
- 变化的因素:状态变化后触发的不同操作
- 方案:操作观察订单变化的主题
4.4 中介模式
定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用。
应用举例
- 消息队列的Broker
- MVC的Controller
- PC里边的主板总线
实例详解
现有大部分的微服务模型对外暴露都是门面,这里不再举例。
门面模式并没有解决耦合,实际上是把耦合封装到一起了。
4.5 责任链模式——如果有多个对象有机会处理请求,职责链可使请求的发送者和执行者者解耦,请求沿着职责链传递,直到有一个对象处理了它为止
应用场景
- 这些参与者,是同源的
- 一个过程中有很多的参与者才能完成,在不同的场景下,参与者的数量和执⾏顺序会不不同
- 要么某个参与者的结果对流程呈现出相同的影响,要么参与者之间需要感知和处理理不同场景的上游结果和下游步骤
- 整个执⾏链条的中间结果,不会被外部感知,不能受外界的流程⽀支配
应用举例
- 流程式的操作
- SpringAop切面执行链
- MVC的拦截器
实例详解
退款的执行流程
4.6 备忘录模式——在不破坏封装性的前提下,捕获一个对象的内部状态并在该对象之外保存这个状态。这样以后就可以利用该保存的状态实施恢复操作
应用举例
数据库快照保存
实例详解
例如在数据库变化时记录一个快照,这里的存储备忘录很有可能也是落入数据库