常用设计模式及其 Java 实现
设计模式是在不断出现的特定情境下,针对特定问题,可以重复使用的特定解决模式(套路)。本文按照创建型、结构型、行为型三大类,总结了常见的 24 种设计模式的使用要点,包括适用场景、解决方案、及其相应的 Java 实现。
作者:王克锋
出处:https://kefeng.wang/2018/04/16/design-patterns/
版权:自由转载-非商用-非衍生-保持署名,转载请标明作者和出处。
1 概述
1.1 概念
设计模式,是在某个不断出现的“情境(Context)”下,针对某个“问题”的某种“解决方案”:
- “问题”必须是重复出现的,“解决方案”必须是可反复应用的;
- “问题”包含了“一个目标”和“一组约束”,当解决方案在两者之间取得平衡,才是有用的模式;
- 设计模式不是法律准则,只是指导方针,实际使用时可以根据需要微调,只是要作好注释,以便他人清楚;
- 很多看似的新模式,实质上是现有模式的变体;
- 模式的选用原则:尽量用最简单的方式设计,除非为了适应未来确实可能的变化,才采用设计模式,因为设计模式会引入更多类更复杂的关系,不要为了使用模式而使用模式。
1.2 六大原则
将六大原则的英文首字母拼在一起就是SOLID(稳定的),所以也称之为 SOLID 原则。
1.2.1 单一职责原则(Single Responsibility Principle)
There should never be more than one reason for a class to change.
一个类只有一个职责,而不是多个职责耦合在一个类中(比如界面与逻辑要分离)。
1.2.2 开放封闭原则(Open Closed Principle)
Software entities like classes, modules and functions should be open for extension but closed for modifications.
对扩展开放,对修改关闭,使用接口和抽象类。
1.2.3 里氏替换原则(Liskov Substitution Principle)
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
确保父类可以出现的地方,子类一定可以出现,这是继承复用的基石。
1.2.4 最少知道原则(Least Knowledge Principle)
Only talk to you immediate friends.
低依赖,各实体尽量独立,尽量减少相互作用。
1.2.5 接口隔离原则(Interface Segregation Principle)
The dependency of one class to another one should depend on the smallest possible interface.
客户(client)应该不依赖于它不使用的方法。尽量使用多个接口分工合成,而不是单个接口耦合多种功能。
1.2.6 依赖倒置原则(Dependency Inversion Principle)
High level modules should not depends upon low level modules.
Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions.
要依赖于抽象(接口或抽象类),而不是具体(具体类)。
1.3 价值
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。
设计模式看似简单问题复杂化。但“简单”的设计灵活性差,在当前项目中不便扩展,拿到其他项目更是无法使用,相当于“一次性代码”。而设计模式的代码,结构清晰,当前项目中便于扩展,拿到其他项目也适用,是通用的设计。
很多程序员接触到设计模式之后,都有相见恨晚的感觉,感觉自己脱胎换骨,达到了新的境界,设计模式可以作为程序员划分水平的标准。
不过我们也不能陷入模式的陷阱,为了使用模式而去套模式,那样会陷入形式主义。
1.4 选用方法
- 每个设计模式,都隐含了几个OO原则,当没有合适的设计模式可选时,可回归到OO原则来取舍;
- 使用模式最好的方式:脑子里装着各种模式,看已有设计或代码中,哪里可以使用这些模式,以复用经验;
- 共享设计模式词汇(包括口头叫法、代码中类与方法的命名)的威力:
(1)与他人沟通时,提到设计模式名称,就隐含了其模式;
(2)使用模式观察软件系统,可以保持在设计层次,而不会被停留在琐碎的对象细节上;
(3)团队间用设计模式交流,彼此看法不容易误解。
1.5 重要书籍
作者:埃里希·伽玛(Erich Gamma), Richard Helm , Ralph Johnson,John Vlissides,后以“四人帮”(Gang of Four,GoF)著称。有两本书:
1.5.1 《Head First 设计模式》
强烈推荐阅读。英文书名是《Head First Design Patterns》。
信耶稣的人都要读圣经,信OO(面向对象)的人都要读四人组的《Head First 设计模式》,官方网站 Head First Design Patterns。2004 该书荣获Jolt奖(类似于电影领域的奥斯卡奖)。
- 是首次将模式归类的功臣,开启了软件领域的一大跃进;
- 模式的模板:包括名称、类目、意图、动机、适用性、类图、参与者及其协作、结果、实现、范例代码、已知应用、相关模式等。
1.5.2 《设计模式:可复用面向对象软件的基础》
英文书名是《Design Patterns: Elements of Reusable Object-Oriented Software》。也是四人组所著。
是软件工程领域有关软件设计的一本书,提出和总结了对于一些常见软件设计问题的标准解决方案,称为软件设计模式。这本书在1994年10月21日首次出版,至2012年3月已经印行40刷。
2 分类与定义
设计模式可分为三个大类,每个大类又包含若干具体的模式。
容易混淆的几种模式:简单工厂S / 抽象工厂A / 工厂方法F / 模板方法T
- “工厂”字样的:带的只用来创建实例,比如 S/A/F;不带的则不限,比如 T;
- “方法”字样的:带的无需额外的客户端参与,可以独立运转,比如 F/T;不带的需要额外的客户端调用,比如 S/A。
2.1 创建型(Creational Patterns)
用于对象的创建,把创建对象的工作放在另一个对象中,或者推迟到子类中。
2.1.1 单例(Singleton)
确保一个类只有一个实例,并提供一个全局的访问点。
需要注意的是,多个类加载器下使用单例,会导致各类加载器下都有一个单例实例,因为每个类加载器都有自己独立的命名空间。
JDK 中的单例有 Runtime.getRuntime()
、NumberFormat.getInstance()
下面总结了四种线程安全的 Java 实现方法。每种实现都可以用 Singleton.getInstance().method();
调用。
2.1.1.1 饿汉方式
关键思路:作为类的静态全局变量,加载该类时实例化。
缺点是真正使用该实例之前(也有可能一直没用到),就已经实例化,浪费资源。
对于 Hotspot VM,如果没涉及到该类,实际上是首次调用 getInstance() 时才实例化。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/**
* @author: kefeng.wang
* @date: 2016-06-07 10:21
**/
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
// 基于 classLoader 机制,自动达到了线程安全的效果
public static Singleton getInstance() {
return instance;
}
public void method() {
System.out.println("method() OK.");
}
}
2.1.1.2 懒汉方式
关键思路:在方法 getInstance() 上实现同步。
缺点是每次调用 getInstance() 都会加锁,但实际上只有首次实例化时才需要,后续的加锁都是浪费,导致性能大降。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/**
* @author: kefeng.wang
* @date: 2016-06-07 10:21
**/
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
public void method() {
System.out.println("method() OK.");
}
}
2.1.1.3 懒汉方式(双重检查加锁)
关键思路:不同步的情况下检查到尚未创建,再同步检查到尚未实例化时,才实例化。以便大大减少同步的情况。
缺点是:要求 JDK5+,否则许多 JVM 对 volatile 的实现导致双重加锁失效。不过现在极少开发者会用 JDK5,所以该缺点关系不大。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/**
* @author: kefeng.wang
* @date: 2016-06-07 10:21
**/
public class Singleton {
private volatile static Singleton instance = null; // 注意 volatile
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) { // 初步检查:尚未实例化
synchronized (Singleton.class) { // 再次同步(对 Singleton.class)
if (instance == null) { // 确认尚未实例化
instance = new Singleton();
}
}
}
return instance;
}
public void method() {
System.out.println("method() OK.");
}
}
2.1.1.4 内部静态类方式(推荐!)
关键思路:全局静态成员放在内部类中,只有该内部类被引用时才实例化,以达到延迟实例化的目的。这是个完美方案:
- 确保延迟实例化至 getInstance() 的调用;
- 无需加锁,性能佳;
- 不受 JDK 版本限制。
1 | /** |
2.1.2 生成器(Builder)
将对象的创建过程,封装到一个生成器对象中,客户按步骤调用它完成创建。
Java 实现请参考 StringBuilder
的源码,这里给出其使用效果:1
2
3StringBuilder sb = new StringBuilder();
sb.append("Hello world!").append(123).append('!');
System.out.println(sb.toString());
2.1.3 简单工厂(Simple Factory) ★
不是真正的“设计模式”。自身是工厂实现类,直接提供创建方法(可多个),可以是静态方法。JDK 中有 Boolean.valueOf(String)
、Class.forName(String)
。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
31
32
33
34
35
36
37
38
39
40/**
* @author: kefeng.wang
* @date: 2016-06-09 19:42
**/
public class DPC3_SimpleFactoryPattern {
private static class SimpleFactory {
public CommonProduct createProduct(int type) { // 工厂方法,返回“产品”接口,形参可无
if (type == 1) {
return new CommonProductImplA(); // 产品具体类
} else if (type == 2) {
return new CommonProductImplB();
} else if (type == 3) {
return new CommonProductImplC();
} else {
return null;
}
}
}
private static class SimpleFactoryClient {
private SimpleFactory factory = null;
public SimpleFactoryClient(SimpleFactory factory) {
this.factory = factory;
}
public final void run() {
CommonProduct commonProduct1 = factory.createProduct(1);
CommonProduct commonProduct2 = factory.createProduct(2);
CommonProduct commonProduct3 = factory.createProduct(3);
System.out.println(commonProduct1 + ", " + commonProduct2 + ", " + commonProduct3);
}
}
public static void main(String[] args) {
SimpleFactory factory = new SimpleFactory(); // 工厂实例
new SimpleFactoryClient(factory).run(); // 传入客户类
}
}
2.1.4 抽象工厂(Abstract factory) ★
一个抽象类,定义创建对象的抽象方法。继承后的多个实现类中,实现创建对象的方法。
客户端灵活选择实现类,完成对象的创建。
JDK 中采用此模式的有 NumberFormat.getInstance()
。
2.1.5 工厂方法(Factory method) ★
创建方法的对于抽象类和实现类的分工,与“抽象工厂”类似。
区别在于:本模式无需客户端,自身方法即可完成对象创建前后的操作。
2.1.6 原型(Prototype)
当创建实例的过程很复杂或很昂贵时,可通过克隆实现。比如 Java 的 Object.clone()
。
2.2 结构型(Structural Patterns)
用于类或对象的组合关系。
2.2.1 适配器(Adapter)
将一个接口适配成期望的另一个接口,可以消除接口不匹配所造成的兼容性问题。
比如把 Enumeration<E>
适配成 Iterator<E>
,Arrays.asList()
把 T[]
适配成 List<T>
。
2.2.2 桥接(Bridge) ★
事物由多个因子组合而成,而每个因子都有一个抽象类和多个实现类,最终这多个因子可以自由组合。
比如多种遥控器+多种电视机、多种车型+多种路况+多种驾驶员。JDK 中的 JDBC
和 AWT
。
2.2.3 组合(Composite) ★
把对象的“部分/整体”以树形结构组织,以便统一对待单个对象或多个对象组合。
比如多级菜单、二叉树等。
2.2.4 装饰(Decorator)
运行时动态地将职责附加到装饰者上。
扩展功能有两种方式,类继承是编译时静态决定,而装饰者模式是运行时动态决定,有独特优势。
比如 StringReader
被 LineNumberReader
装饰后,为字符流扩展出了 line
相关接口。
2.2.5 外观(Facade) ★
提供了一个统一的高层接口,用来访问子系统中的一群接口,让子系统更容易使用。
比如电脑的启动(或关闭),是调用CPU/内存/磁盘各自的启动(或关闭)接口。
2.2.6 享元 / 蝇量(Flyweight)
运用共享技术有效地支持大量细粒度的对象。
比如文本处理器,无需为每个字符的多次出现而生成多个字形对象,而是外部数据结构中同一字符的多次出现共用一个字形对象。
JDK 中的 Integer.valueOf(int)
就采用此模式。
2.2.7 代理(Proxy)
proxy 创建并持有 subject 的引用,client 调用 proxy 时,proxy 会转发给 subject。
比如 Java 里的 Collections
集合视图、RMI/RPC 远程调用、缓存代理、防火墙代理等。
2.3 行为型(Behavioral Patterns)
用于类或对象的调用关系。
2.3.1 责任链(Chain of responsibility)
一个请求沿着一条链传递,直到该链上的某个处理者处理它为止。
比如 SpringMVC 中的过滤器。
2.3.2 命令(Command)
将命令封装为对象,可以随意存储/加载、传递、执行/撤消、排队、记录日志等,将“动作的请求者”从“动作的执行者”中解耦。
参与方包括 Invoker(调用者) => Command(命令) => Receiver(执行者)。
比如定时任务、线程任务 Runnable
。
2.3.3 解释器模式(Interpreter)
用于创建简易的语言解释器,可处理脚本语言和编程语言,为每个规则创建一个类。
比如 JDK 中的 java.util.Pattern
、java.text.Format
。
2.3.4 迭代器(Iterator)
提供一种方法,顺序访问一个聚合对象中的各个元素,而无需暴露其内部表现。
比如 JDK 中的 java.util.Iterator
和 java.util.Enumeration
。
2.3.5 中介者(Mediator)
使用一个中介对象,封装一系列的对象交互,中介对象使各对象无需显式相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
比如 JDK 中的 java.util.Timer
和 java.util.concurrent.ExecutorService.submit()
。
2.3.6 备忘录(Memento)
备忘录对象用来存储另一个对象的内部状态的快照,并可在外部存储起来,之后可还原到当初的状态。比如 Java 序列化。
比如 JDK 中的 java.util.Date
和 java.io.Serializable
。
2.3.7 观察者(Observer)
对象间一对多的依赖,被观察者状态改变时,观察者都会收到通知。
参与方包括 Observable(被观察者) / Observer(观察者)。
比如 RMI 中的事件、java.util.EventListener
。
2.3.8 状态(State)
对象的内部状态变化时,其行为也随之改变。其内部实现是,定义一个状态父类,为每种状态扩展出状态子类,当对象内部状态变化时,所选的状态子类也跟着切换,但外部只需与该对象交互,而不知道状态子类的存在。
比如视频播放器的停止/播放/暂停等状态。
2.3.9 策略(Strategy)
定义一组算法,分别封装起来,独立于客户之外,算法更改时不影响客户使用。
比如游戏中的不同角色,可以使用各种装备,这些装备可以策略的方式封装起来。
比如 JDK 中的 java.util.Comparator#compare()
。
2.3.10 模板方法(Template method) ★
抽象类中定义顶级的逻辑框架(叫做“模板方法”),一些步骤(可以创建实例或其他操作)延迟到子类实现,自身可独立运转。
当子类实现的操作是创建实例时,模板方法就变成了工厂方法模式,所以说工厂方法是特殊的模板方法。
2.3.11 访问者(Visitor) ★
在不修改被访问者数据结构的前提下,访问者中封装访问操作,关键点是被访问者中提供被访问的接口。
适用场景是被访问者稳定但访问者灵活多变,或者访问者有多种不同类的操作。
2.4 复合模式(Compound)
结合两个或更多模式,组成一个解决方案,解决经常发生的一般性问题。
使用案例:MVC模式(Model/View/Controller),使用了观察者(Observer)、策略(Strategy)、组合(Composite)、工厂(Factory)、装饰器(Decorator)等模式。
使用案例:家用电器=界面+数据+逻辑控制,商场=店面+仓库+逻辑控制。
3 参考文档
维基百科: 设计模式
Wikipedia: Software design pattern
TutorialsPoint: Design Pattern