设计模式是前辈们针对实际编码中各种问题对应解决方案的抽象总结,是一种最佳实践。因此值得每一位工程师学习借鉴。 使用的时候重点是识别面临的问题的场景,识破问题关键,挑选适当的模式进行编码。 识别这一步最为关键。

wiki/Software_design_pattern 针对软件设计模式的发展历史有详细说明,简单来说,GoF四人帮大佬们在1994年发布了书籍 Design Patterns: Elements of Reusable Object-Oriented Software ,这本书对于设计模式的工业落地、推广具有重大意义。

设计模式在工业界实践已久,但是长久以来未能取得进展。

本文以GoF版本为准,这也是工业界使用最广泛的标准化解释。本文使用Java示例,示例仓库:design-pattern-java

设计模式一共23种,分三个大类:

  • Creational 创建型

    「不直接创建对象,定义对应的创建逻辑」,解耦对象的创建、使用。

  • Structural 结构型

    「关注类、对象间如何组合,一般利用语言特性(比如继承、实现、组合)或者平台特性(agent、字节码)进行结构组装」,解耦不同功能。

  • Behavioral 行为型

    「关注对象间如何交互、通信」,解耦不同行为。

编程指导原则参见 编程原则一览

Creational 创建型

Singleton 单例

Singleton 单例

restricts object creation for a class to only one instance.

用于创建全局唯一的对象。

全局可区分不同粒度:

  • 线程
  • 进程
  • 集群

目的:限制对象个数。

Prototype 原型

Prototype 原型

creates objects by cloning an existing object.

描述:

基于一个已有对象实例,克隆、复制一个新的对象。

适用场景:

  1. 新旧实例有很多相似之处,可以复制;
  2. 新实例重新创建有过多开销;
  3. 提供一定的业务语义,比如XNew对象来源于XOld对象,有关联性;

此时可考虑原型。

使用

实现接口Cloneable,重写clone方法。

java本身提供的clone调用的是本地方法,有性能优势,缺点是默认只支持浅拷贝。需要深拷贝一般通过序列化、反序列化实现。

Builder 构建者

Builder 构建者

constructs complex objects by separating construction and representation.

描述: 分离构造与表示,以此来创建复杂对象。

支持一步步构建对象,将构建过程细化,支持创建多种不同属性值的实例。

适用场景:

  1. 对象属性过多时,使用构建者替换构造器,更加清晰、灵活;

使用

推荐直接使用@Lombok注解@Builder,利用 JSR269 插件化注解处理的接口,帮我们在生成字节码之前通过注解自动生成代码。

Factory method 工厂方法

creates objects without specifying the exact class to create.

Abstract factory 抽象工厂

groups object factories that have a common theme.

描述:

  • 工厂方法:不指定特定类,将实例创建交给工厂。
  • 抽象工厂:将具有共同主题的工厂组合起来。

工厂模式用于创建不同但是同类型的对象(OOP中的接口或者父类)。

相比工厂方法,抽象工厂中的抽象我理解是抽象了共性、层次 工厂创建的对象有共性,工厂也有共性。

比如轮胎有多种品牌、型号,生产轮胎的厂商也有多个。

而厂商除了轮胎,还可以生产底盘。轮胎、底盘、厂商,有层次。 在生产汽车的工厂中,定义的接口可能是:生产轮胎,生产底盘。产品间也有层次,汽车是他们共同的主题。

中文语境下的简单工厂、工厂方法、抽象工厂其实本质上只是所需工厂的复杂度、维度不同。越往后,复杂度、对象层次维度越高。

目的:解耦对象的创建、使用。

Structural 结构型

Adapter 适配器

allows classes with incompatible interfaces to work together by wrapping its own interface around that of an already existing class.

描述: 适配器模式通过包装已有接口的方式利用已有功能,使得新旧不兼容的接口能够对接生效。

适用场景:

  1. 新老接口定义不兼容(签名不一致);

    有一种情况是有一个接口设计有缺陷。

  2. 新接口可复用老接口功能实现;

  3. 统一多个接口;

适配器目的主要是补救、兼容。

现实举例: 电源适配器,不同地区标准不同的转接头。 港版mbp电源适配器

场景举例: 我买了一台港版MacBook,一般的电源适配器负责对接笔记本与电源接口,而港版的电脑如果想充电,我就需要再买一个转接头。

  • 内地电源插线板对应我们的老接口;InlandPatchBoardI
  • 港版插头对应我们的新接口;HongkongPlugI
  • 这个国标转接头对应我们这里的适配器;HongkongPowerAdaper

也就是说,我们先有一个老接口(国标插线板),然后新需求产生了一个新接口(港版插头),此时新增一个国标转接头,新老接口就完成了对接,我们的改动成本也相对可控。

图例 通过图、代码的方式对比下类适配器与对象适配器的区别。

类适配器: HongkongClsAdapter

对象适配器: HongkongObjectAdapter

使用入口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class AdapterApp {
    // 插线板通电
    public static void main(String[] args) {
        // 内地默认逻辑
        InlandPatchBoardI inlandPatchBoardGeneral = new InlandPatchBoardImpl();
        inlandPatchBoardGeneral.powerOn();

        // 港版,class型适配器
        InlandPatchBoardI hongkongPatchClsPlug = new HongkongClsAdapter();
        hongkongPatchClsPlug.powerOn();
        // 港版,object型适配器
        InlandPatchBoardI hongkongPatchObjectPlug = new HongkongObjectAdapter(new HongkongPlugImpl());
        hongkongPatchObjectPlug.powerOn();
    }
}

可以看到二者区别为: 类模式使用继承的方式交互,对象模式使用组合的方式交互。一般推荐使用组合,即对象模式实现适配器。

使用举例

SpringMVC-WebMvcConfigurerAdapter

使用SpringMVC的工程如果需要增加拦截器,我们可以实现接口 WebMvcConfigurer,由于Java以前接口必须实现所有非默认方法,所以实现类中必须写出所有接口方法,如果继承 WebMvcConfigurerAdapter,则只需要覆写 addInterceptors 一个方法。

由于Java8支持了interface提供默认方法,关键字 default ,默认方法实现类无需必须实现。因此后续SpringMVC废弃了 WebMvcConfigurerAdapter

ApacheCommonsIO-FileAlterationListenerAdaptor

apache的commons-io包下FileAlterationListener,提供了查看文件状态变更的接口。FileAlterationListenerAdaptor 则是对应的适配器实现。

当我们想要监控文件状态变更时,继承FileAlterationListenerAdaptor是一个更好的选择。

小结

  • 适配器最简单的为默认适配器(上述框架使用例子);
  • 对象模式与类模式,一般推荐使用组合,即对象模式实现适配器;
  • 与代理模式简单对比:
    • 比较对象适配器模式和代理模式。在代码结构上,它们很相似,都需要一个具体的实现类的实例。但是它们的目的不一样,代理模式做的是控制访问原方法的活;适配器做的是适配的活,为的是提供“把港版电源线包装成国标的线,然后当做国标电源来使用”,而港版的电源和国标电源之间原本没有继承关系。

Bridge 桥接

decouples an abstraction from its implementation so that the two can vary independently.

通过抽象接口,与实现解耦,以此达到互相变化不影响的目的。

Composite 组合

composes zero-or-more similar objects so that they can be manipulated as one object. GoF: Compose objects into tree structure to represent part-whole hierarchies.Composite lets client treat individual objects and compositions of objects uniformly.

组合多个相似对象为树形结构,表示:「部分、整体」的层次。简化使用方的调用。

注意:组合模式与OOP中的组合两个类不一样!

场景:

  • 操作系统的文件元信息表示是INode,本质上「目录-文件」是树形结构;
  • InnoDB的索引结构是B+树,也是树形结构;
  • 我们的业务应用一般有菜单,菜单一般是树形;
  • 我们的人员组织架构,一般是树形;

代码示例: design-pattern-java Composite

在这里例子中,我们的部门与员工统一表示为组织节点OrgNode,员工+部门组成树形结构,其中员工、部门分别是「部分」表示,组合在一起形成「整体」。我们的操作统一到了OrgNode中,简化了调用。

Decorator 装饰

dynamically adds/overrides behaviour in an existing method of an object.

Facade 门面

provides a simplified interface to a large body of code.

Flyweight 享元模式

reduces the cost of creating and manipulating a large number of similar objects.

针对不可变对象:

  • 复用对象
  • 减少内存开销
  • 减少创建开销

适用场景:

  • 系统中存在大量重复对象,并且对象是不可变的,此时可实现对象实例的享元,即一个对象大家共用。
  • 在不同的粒度可以实现不同的享元,如细化到属性维度。

目的:复用对象、节省内存。

Proxy 代理

provides a placeholder for another object to control access, reduce cost, and reduce complexity.

代理对象持有原对象的引用,用于:

  • 控制访问;
  • 降低开销;
  • 降低复杂度;

Behavioral 行为型

Chain of responsibility 职责链

delegates commands to a chain of processing objects. Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.

  1. 将命令委派到处理器的链条上。
  2. 通过「将请求交给多个对象处理」的方式,来避免耦合请求的生产者、消费者。
    • 消费者将请求在链条上逐个传递。「GoF定义」
    • 链条可选择某个消费者处理后终止,或者继续传递直到所有消费者都处理完毕。 「变体」

优势:

  • 复用逻辑。
  • 使用扩展的方式增加功能:高内聚、低耦合「灵活」。

应用:

Command 命令

creates objects that encapsulate actions and parameters. The command pattern encapsulates a request as an object, thereby letting us parameterize other objects with different requests, queue or log requests, and support undoable operations.

  1. 使用对象封装行为、参数。
  2. 命令模式将请求封装成对象,不同的请求对应不同对象,从而将参数内聚到某个类中。支持队列操作、日志记录、回滚「附加功能」。
  3. 核心作用:为命令执行增加功能。

Interpreter 解释器

implements a specialized language. Interpreter pattern is used to defines a grammatical representation for a language and provides an interpreter to deal with this grammar.

描述:

  1. 实现特定语言。
  2. 解释器模式用于为一种特定语言提供语法表示,提供解析语法的解释器。
  3. 应用偏小众:编译器、规则引擎、正则表达式。

实现:

  1. 解释器主要实现语法解析。
  2. 针对负责解析,遵循大拆小的基本策略。

Iterator 迭代器

accesses the elements of an object sequentially without exposing its underlying representation.

Mediator 中介

allows loose coupling between classes by being the only class that has detailed knowledge of their methods. Mediator pattern defines a separate object that encapsulates the interaction between a set of objects and the objects delegate their interaction to a mediator object instead of interacting with each other directly.

描述:

  1. 中介作为唯一知道上下游对接细节的类「对象」,从而做到解耦上下游对接。
  2. 中介模式定义一个中介对象,封装对接双方交互的逻辑,对接双方不直接交互,只能通过中介操作。

现实举例:

  1. 飞机间互相通信,统一通过塔台调度。
  2. 多端数据互相同步,统一通过log数据平台交互。

优点:

  1. 集中化管理交互逻辑,简化交互调用。

缺点:

  1. 中介类可能变为「上帝类」,复杂而庞杂。

Memento

provides the ability to restore an object to its previous state (undo). Captures and externalize an object’s internal state so that it can be restored later, all without violating encapsulation.

  • 备忘录模式,aka快照模式「snapshot」。
  • 赋予对象回滚状态的能力。
  • 在不违背封装原则的前提下,捕获一个对象的内部状态,以便后续回滚状态使用。

类比:

  • Redis中的RDB、AOF。是典型的高频增量、低频全量快照优化实现。
  • 存储系统中的wal预写日志。

Observer 观察者

is a publish/subscribe pattern, which allows a number of observer objects to see an event. Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

描述:

  • 一种发布订阅模式,实现了一组观察者对象,用于监听事件变化。
  • 定义对象间的一对多关系,当前者对象状态发生变化时,后者对象组可自动收到通知。

结构、叫法:

被观察者 Observable Subject Publisher Producer EventEmitter Dispatcher
观察者 Observer Observer Subscriber Consumer EventListener Listener

目的:解耦观察者、被观察者。

复杂实现:Google EventBusSpring event

State 状态

allows an object to alter its behavior when its internal state changes.

Strategy 策略

allows one of a family of algorithms to be selected on-the-fly at runtime. Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

定义:

  • 运行时选择具体执行的算法。
  • 定义封装一组算法,彼此可替换。策略模式允许使用侧与算法的变化互相独立。

目的:解耦策略的定义、创建、使用。

Template method 模板方法

defines the skeleton of an algorithm as an abstract class, allowing its subclasses to provide concrete behavior.

定义: 通过抽象类的方式定义类骨架,通过子类实现具体算法。

目的:

  • 骨架:复用结构。
  • 子类实现:拓展逻辑。

示例: 定义骨架、规范:

拓展:

Visitor

separates an algorithm from an object structure by moving the hierarchy of methods into one object.

对比

设计模式最大的区分在于设计意图、应用场景。回到本文最开始的观点:重点是识别面临的问题的场景,识破问题关键

享元 vs 单例 vs 缓存 vs 池

设计意图
享元 复用对象「共享一个对象的同时使用权」、节省内存
单例、多例 限制对象个数
缓存 提高访问效率
重复使用「共享一个对象的非同时使用权」、节省时间

代理 vs 桥接 vs 装饰器 vs 适配器

设计意图
代理 控制访问
桥接 分离接口与实现,隔离变化
装饰器 增强原接口功能
适配器 事后补救,兼容新老接口

模板方法 vs 同步回调 vs 异步回调

应用场景 代码实现 优点
模板方法 定义骨架,定制逻辑 基于继承,子类重写父类方法
同步回调 定义骨架,定制逻辑 基于组合,传递回调对象 灵活
异步回调 与观察者模式一致 基于组合,传递回调对象 灵活

策略 vs 工厂 vs 命令

应用场景、设计意图 策略 vs 工厂 策略 vs 命令
策略 解决根据运行时状态从一组策略中选择不同策略的问题 包含策略的定义、创建、使用。结构与工厂类似 不同策略可互相替换,结果一致
工厂 封装对象创建过程
命令 控制命令的执行:异步、延迟、队列、回滚、日志 将函数封装成对象。 不同命令不可互相替换,结果不一致

中介 vs 观察者

应用场景 区别
中介 交互复杂,解耦对象间的交互 交互关系复杂,上下游流向可互相转换,如a->b 转换为 b->a
观察者 明确的一对多关系下,解耦观察者、被观察者 观察者被观察者关系明确,上下游流向固定不可变

Ref