I. 引言
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。
使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基础,如同大厦的结构一样。
创建型模式:对类的实例化过程的抽象。一些系统在创建对象时,需要动态地决定怎样创建对象,创建哪些对象,以及如何组合和表示这些对象。创建模式描述了怎样构造和封装这些动态的决定。包含类的创建模式和对象的创建模式。
结构型模式:描述如何将类或对象结合在一起形成更大的结构。分为类的结构模式和对象的结构模式。类的结构模式使用继承把类,接口等组合在一起,以形成更大的结构。类的结构模式是静态的。对象的结构模式描述怎样把各种不同类型的对象组合在一起,以实现新的功能的方法。对象的结构模式是动态的。
行为型模式:对在不同的对象之间划分责任和算法的抽象化。不仅仅是关于类和对象的,并是关于他们之间的相互作用。类的行为模式使用继承关系在几个类之间分配行为。对象的行为模式则使用对象的聚合来分配行为。
设计模式分为三种类型,共23种。
创建型模式:单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式。
结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
行为型模式:模版方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、责任链模式、访问者模式。
下面将对一些常用的设计模式和软件设计的六大原则进行介绍。
一. 创建型模式
1. Factory method 工厂方法(工厂模式)
作用
一个类需要一个产品来完成某项工作,但它不能确定,也不关心具体拿到什么产品,因此它定义一个工厂方法,将具体产品的生产延迟到子类决定。
实现
- 父类可以选择为工厂方法提供一个默认的实现;
- 工厂方法通常在模板方法(Template method)中被调用,上图中
AnOperation()
就是一个模板方法。
2. Abstract factory 抽象工厂
作用
系统有一组相互关联的产品接口,及几套不同的实现。客户只依赖产品接口,并需要能灵活地在几套实现中切换。
因此提供一个抽象工厂生产抽象的产品,每个产品在其中都对应一个工厂方法,产品族的每一套实现都提供一个具体工厂。客户通过抽象工厂获取产品,当需要切换到产品的其他实现时只需要更换工厂的实现类。
实现
应用
根据底层数据源的不同,DAO的实现通常有几套,当切换数据源时,系统使用的DAO的实现也应当能快速切换。这是使用抽象工厂的一个典型场景。
3. Singleton 单例
作用
保证一个类只有一个对象
实现
-
private
构造器 -
private static
类变量 singleton -
public static
类方法getInstance()
返回singleton。
实例化时机:
- eager
- lazy
lazy init 多线程问题的解决办法:Double Check
private volatile static A singleton = null;
public static A getInstance(){
if(singleton == null){
sychronized(A.class){
if(singleton == null) singleton = new A();
}
}
return singleton;
}
private A(){}
- 为什么要第二次的null判断?
在第一次判null / 获取锁之间可能有其他线程实例化了。 - 为什么要
volatile
?
在上面提到的情况下,如果没有volatile
保证的可见性,在第二次null判断时当前线程可能看不到别的线程创建的对象,从而通过并再创建一次。
static 内部类
利用 “类的加载/static块是线程安全的” 实现线程安全的lazy init:
public class A{
private static class Holder{
private static A singleton = new A();
}
public static A getInstance(){
return Holder.singleton;
}
private A(){}
}
4. Builder 建造者模式
作用
你有一个产品,该产品由若干part装配而成,装配的逻辑是固定的,但各个part的构造是可切换选择的,Builder模式将 固定的装配逻辑 与 易变的part构造逻辑 分离开,可以方便地在不同的part实现逻辑之间切换。
实现
-
Director#construct()
负责固定的装配逻辑; - 一个
Builder
实例负责一个产品内部所有part的构造(buildPart()
方法族),并向外部暴露方法,在part都装配完毕后获取该产品。
交互
5. Prototype 原型模式
作用
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
适用性
当要实例化的类是在运行时刻指定时,例如,通过动态装载;
为了避免创建一个与产品类层次平行的工厂类层次时;
当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。
实现
二. 结构型模式
1. Adapter 适配器
作用
在两个不兼容的接口之间加一个中间层,用组合的方式将一个现有对象匹配到需要的接口。
Convert the interface of a class into another interface the client expects
实现
2. Proxy 代理
作用
Provide a surrogate or place holder for another object to control access to it
你有一个真正干活的对象RealSubject,但需要向client控制对他的访问,比如权限的控制 / Lazy load / 结果的缓存等等,因此在client和RealSubject之间增加一个中间层Proxy代替RealSubject,Proxy包裹RealSubject,将具体功能实现委托给它,并在RealSubject执行真正的功能前后插入自己的逻辑;此外,Proxy向client隐藏了RealSubject的存在。
实现
与Decorator
模式的区别
Proxy与Decorator有着相似的结构,** 他们都在client和真实对象之间增加一个与真实对象实现了相同接口的中间层,这个中间层保留了对真实对象的引用并向他们发送请求**。然而他们的设计目的是不同的:
Decorator侧重动态为实体增加功能,因此在该模式中:
- 实体只实现了部分功能,Decorator实现了其他的增强功能;
- 支持递归组合(增加多重功能);
- Decorator不知道自己装饰的是哪个具体对象,client必须自己手动将实体和Decorator关联起来。
Proxy的目的则是当访问一个特定实体不方便或不符合要求时,为这个实体提供一个替代者,因此:
- 实体实现了关键功能,Proxy提供(或拒绝)对它的访问;
- 不支持递归组合;
- Proxy向client屏蔽RealSubject的存在,client只能拿到Proxy;
- Proxy确定地知道自己的代理目标是RealSubject,因此它和RealSubject相关联而不是Subject接口;此外,它们的关系是静态的,无法在运行时改变Proxy代理的目标对象。
3. Bridge 桥接模式
作用
你有一个产品,它在两个维度上都是可变化的,如果用继承,则需要n*m个子类。Bridge模式将两个维度的继承体系独立出来,并在二者之间用组合进行装配,避免类的泛滥。
进一步地考虑,一个产品的继承体系应该只有一个维度,如果出现了其他维度上的继承,要考虑该维度是否是行为/实现相关的。对于行为/实现方面的变化,应当先把行为独立地抽象出来,并与原产品组合(这就是策略模式
的含义),而不应该直接在原产品上通过继承表达该行为的变化。
举个例子,假如系统内要发送消息,消息按迫切程度分为普通/加急/特急,消息的发送形式也可以多样,比如站内信/短信/email,每种消息都要求可以用任意方式发送:
如果简单地用继承,则需要33 = 9个类。但实际上,消息的发送* 这个维度属于行为,不要用继承来表达行为的变化,这样会污染原本的抽象层次,应当用策略模式
将 消息发送 这个行为分离。
采用Bridge模式:
实现
产品的抽象 + 行为的分离(策略模式
)
总结
Bridge模式在我看来是对策略模式
的扩展,它的核心有两点:
- 只在一个维度上用继承,出现了多个维度则考虑分离并用组合,避免类的泛滥和抽象维度的混杂;
- 用
策略模式
隔离行为的变化,不要让行为/实现的变化污染原本的继承体系。
4. Decorator 装饰者
作用
有一系列产品,你希望动态地为他们添加额外 / 可自由组合 的功能,并且不影响产品本身。
Attach additional responsibilities to an object dynamically.
实现
-
Decorator 装饰器
继承产品抽象接口,并在内部持有一个产品(可能是具体产品,也有可能被装饰过了); -
Decorator
的具体实现,为其装饰的产品提供额外的功能,类似递归的调用; - 可以同时反复应用多个
Decorator
,实现额外功能的动态组合。
应用
-
java的IO流的设计是一个典型的装饰者模式:
ByteArrayInputStream | FileInputStream | ObjectInputStream | StringBufferInputStream
是具体的输入流产品,根据数据来源区分;
FilterInputStream
是装饰器;
BufferedInputStream | DataInputStream | LineNumberInputStream | PushbackInputStream
是具体的装饰器,分别为其他输入流提供缓冲/类型读写/跟踪行号/退回已读数据的功能,这些装饰器是可以组合使用的:
InputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("test.txt")));
-
Decorator模式
也可以用来实现AOP
的类似功能(虽然实际大部分都是用JDK动态代理 / 运行时修改字节码
),Decorator
的具体实现就是我们想要独立出来的切面,产品的具体实现则是我们想要保持独立的业务逻辑。
5. Composite 组合模式
作用
实现树形结构,并让用户可以用统一的接口对待叶子节点和非叶子节点。
实现
- 操作孩子的方法应该放在Component中吗?毕竟Leaf是不支持这些操作的。
出于透明性考虑,应该放在Component中,Leaf对这些方法就提供一个空的实现。 - Component除了保存孩子,也可以记录父亲;
应用
UI / 人员组织管理这种典型的树形结构中用的比较广泛。
6. Facade 门面模式
作用
一个系统对外提供服务,系统暴露的接口应该是简单而统一的,客户不应该直接和系统内复杂的子部件进行交互,而应只依赖于一个单一的高层接口,该组件为客户屏蔽了内部的复杂性,降低了客户和系统的耦合。
更像是一种设计思路,而非一个具体模式。
三. 行为模式
1. Chain of Responsibility 责任链
作用
客户端发出一个请求,有一系列的处理器都有机会处理这个请求,但具体哪个是运行时决定的,客户端也不知道究竟谁会来处理。
因此将所有处理器组成一个链条,将请求从链条中流过,每个处理器查看是否应该处理它,如果不是,则交给后面的处理器,否则处理并退出。处理器在链中的位置决定其优先级。
将请求者和处理者解耦,可以动态切换/组合处理者。
实现
扩展
客户端发出一个请求,请求的处理分为很多步骤,这些步骤是不确定的/可以动态组合的,甚至需要支持在运行时改变步骤,或者在步骤间任意跳转。
解决方案和责任链类似,将处理流程抽象为一个处理器链条,链条的组装交给外部决定。每个处理器对请求完成自己负责的业务逻辑,并看情况结束/传递给下一个处理器/跳转到任意处理器。
这和标准的责任链的结构基本一样,但他们的目的不一样。标准责任链目的是动态 找到请求的处理者 ;扩展(某些地方称为“功能链”?)则是为了获取 动态拼装和改变处理流程 的能力。
应用
标准责任链
UI中的事件冒泡机制是责任链的一个典型应用。HTML中,点击一个DOM元素,产生的click事件将依次冒泡给它的父元素,每个父元素上都可以注册对click事件的监听器,监听器中除了对事件处理外,也可以结束事件的继续冒泡。
扩展(功能链)
- Web应用中的各种filter/拦截器;
- Netty中的pipeline
2. Command 命令
Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
你需要向一个对象提交请求,但对请求的处理是动态的,无法写死。比如一个菜单项,在不同的上下文中,点击它要做的事情显然是不一样的。
Command模式的思路是 抽象请求(及处理):
-
Client
装配Invoker
和Command
,** 如果需要不同的处理,装配不同的Command即可 **; -
Client
向Invoker
发出请求; -
Invoker
将请求的处理委托给Command#execute()
。
很多时候Command不够智能,自己无法处理请求,需要将请求委托给另一个Receiver进行真正的处理,ConcreteCommand可以认为是Receiver的适配器:
可以看到,Command模式的最大价值在于:隔离 请求的接收者
和 请求的处理逻辑
;
此外,将请求及其处理逻辑抽象为Command后可以做很多有意思的事情:
1. 可撤销的操作
在Command接口中增加一个接口 undo()
实现单个命令的撤销动作,并用一个stack
保存所有Command;当用户触发撤销时依次从stack pop出最近的Command,执行其undo()
方法。
2. 宏命令
宏命令实质是个树形结构,对Command应用Composite 组合模式
即可实现:
3. 排队
4. 日志记录和恢复
3. Memento 备忘录
有些情况下你需要记录一个对象(称为Originator
)在某个时刻的状态(snapshot),以便后续恢复,我们可以用一个类Memento
表示snapshot,它包含了Originator的部分或全部状态:
- Originator 负责创建Memento,以及恢复到某个Memento;
- Memento 即Originator的snapshot;
- Caretaker 充当协调者,它负责向Originator请求当前Memento / 保存Memento / 在后续某个时刻让Originator恢复到某个Memento。
但这里有一个问题,为了隐藏Originator的实现细节,Memento
必须向外部隐藏内部数据,即不开放state
的 getter/setter 给外部,但这样一来,Originator也无法创建Memento
了。
为了解决这个问题,在Memento模式的一般实现中,Memento
类被分为两个部分:
- 标记接口
Memento
,空的,Caretaker
只能得到这个接口; - Memento的真正实现
MementoImpl
,作为Originator
的 私有内部类 ,这样既允许Originator访问Memento的内部状态,又满足了Memento向外部(主要是Caretaker)隐藏内部细节的要求。
如果对 Memento
的封装性没有严格的要求,第一种实现显然更简单。
4. Observer 观察者
作用
定义一个一对多关系,在Subject状态发生改变时,所有Observer获得通知。
解耦 事件发生者 & 事件接收者,使得双方的改动互不影响,关联关系也可动态改变。
实现
数据传递的两种方式:
- 推:由 Subject 主动向 Observer 推送信息,而不管信息对后者而言是否需要/是否足够。
- 拉:Subject 把自己传递给 Observer,由 Observer 从 Subject 拉取自己需要的信息。
扩展: Observer 注册时可以指定自己感兴趣的事件。
扩展:EventBus
传统的Observer模式中,事件发生者和接收者依然存在耦合,发生者需要管理接收者的集合,我们可以进一步地,在Subject和Observer间增加一个中间层负责转发事件,将它们彻底地解耦;进一步,这个事件转发者可以是通用的,支持任意发布者和接受者,通常称之为EventBus
,是一种广泛应用的架构。
5. State 状态模式
作用
模拟状态机,描述一个对象(Context)的状态变迁,将特性状态下的行为分割开来,避免在Context中用大量的if维护所有状态的变迁,而且容易扩展新的状态。
实现
-
Context
中记录它自己当前的状态; -
Context
接收一个输入动作,并将该输入委托给当前所处State
处理; -
State
处理输入,根据需要让Context
跃迁到另一状态。
如果State
不保存状态则可以是单例的,Java中,可以用enum
类型实现State
。
6. Strategy 策略模式
作用
你有一个对象负责完成某件事情,但在不同时刻其使用的算法是不同的,Strategy模式将可变的算法独立并封装,避免大量if条件判断,并方便替换和扩展。
Strategy
封装了 相同行为的不同实现
实现
Strategy
的实现通常依赖Context
的数据,后者在调用前者的方法时需要将自己传递过去。
实际应用中,经常会发现不同的策略其算法骨架类似,只有某些具体步骤不同,此时可以对Strategy
应用Template Method
模式。
7. Template Method 模板方法
作用
将一个算法的通用骨架抽象到父类以避免代码重复,而将一些可变的步骤延迟到子类,子类不用关心算法结构,只需关注自己需要实现的特定步骤。
实现
这个没什么好说的。
四. 设计原则
-
SRP原则 - 单一职责原则(Single Responsibility Principle)
每个类实现单一职责,并且单一职责都有清楚明确的定义,有利于降低软件的复杂性, 提高可读性、可维护性和可扩展性。
软件设计应遵守单一职责原则,将不同的职责封装到不同的类或模块中。
-
OCP法则 - 开放-关闭原则
一个类应当对扩展开放,对修改关闭。即当有新的需求时,不是修改已有的类,而是对已有的类进行扩展。
- 通过扩展已有软件系统,可以提供新的行为,以满足对软件的新的需求,使变化中的软件有一定的适应性和灵活性。
- 已有软件模块,特别是最重要的抽象层模块不能再修改,这使变化中的软件系统有一定的稳定性和延续性。
实现开闭原则的关键在于 分离不变和变化的部分,并对变化的部分进行合理的高层抽象,并让不变的部分依赖该高层抽象,这样就能在不同的实现间切换,或者扩展新的实现。很多设计模式都体现了这一点,比如策略模式
将算法抽象出来,模板方法
将不变的算法骨架与易变的需要自定义的步骤隔离,装饰者模式
将不变的核心功能对象和易变的增强功能隔离等等。
LSP法则 - 里氏替换原则(Liskov Substitution Principle)
把抽象接口和实现分离,子类必须能替换掉父类,这个原则通常由语言保证。DIP法则 - 依赖倒置原则(Dependence Inversion Principle)
抽象不应当依赖于细节;细节应当依赖于抽象;要针对接口编程,不针对实现编程。具体讲就是要依赖于抽象,不要依赖于具体。
高层不直接依赖底层,而是高层定义自己需要底层提供什么样的接口,底层负责实现,这样就可以随意切换底层的具体实现而不用影响高层,但底层反而要依赖高层公布的接口,所以称为“依赖倒置”。ISP原则 - 接口分离原则 (Interface Segregation Principle)
每一个接口应该是一种角色,不多不少,不干不该干的事,该干的事都要干。这类似编码原则中的最小权限法则。软件设计不应出现庞大的接口,迫使客户在使用时必须从一大堆它不需要的方法中寻找目的方法。这样的接口应该按照不同客户的需求被分离成若干小接口。-
LKP原则 - 最少知识原则(Least Knowledge Principle)
也称为迪米特法则。原则的核心思想即一个对象应对其他对象有尽可能少的了解。
类应当只与自己的朋友交互。该原则的思想是,将类对外部的了解尽量保持在一定范围内,尽量减少类之间的交互,从而降低各个组件间的耦合。
“朋友”的定义:
- 当前对象的属性
- 当前对象所创建的对象
- 方法参数传递进来的参数
- 方法内创建的对象
五. 参考与推荐
- 《大话设计模式》
- 《深入浅出设计模式》
- 《Java与模式》
- 《敏捷软件开发原则、模式与实践》
- 《设计模式--可复用面向对象软件的基础》
- 《设计模式精解》