常用设计模式的总结
一. 创建型模式
1. Factory method 工厂方法
作用
一个类需要一个产品来完成某项工作,但它不能确定,也不关心具体拿到什么产品,因此它定义一个工厂方法,将具体产品的生产延迟到子类决定。
实现
- 父类可以选择为工厂方法提供一个默认的实现;
- 工厂方法通常在模板方法(Template method)中被调用,上图中
AnOperation()
就是一个模板方法。
2. Abstract factory 抽象工厂
作用
系统有一组相互关联的产品接口,及几套不同的实现。客户只依赖产品接口,并需要能灵活地在几套实现中切换。
因此提供一个抽象工厂生产抽象的产品,每个产品在其中都对应一个工厂方法,产品族的每一套实现都提供一个具体工厂。客户通过抽象工厂获取产品,当需要切换到产品的其他实现时只需要更换工厂的实现类。
实现
应用
根据底层数据源的不同,DAO的实现通常有几套,当切换数据源时,系统使用的DAO的实现也应当能快速切换。这是使用抽象工厂的一个典型场景。
3. Singleton 单例
作用
保证一个类只有一个对象
实现
private
构造器private static
类变量 singletonpublic 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都装配完毕后获取该产品。
交互
二. 结构型模式
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,每种消息都要求可以用任意方式发送:
如果简单地用继承,则需要3*3 = 9个类。但实际上,消息的发送 这个维度属于行为,不要用继承来表达行为的变化,这样会污染原本的抽象层次,应当用策略模式
将 消息发送 这个行为分离。
采用Bridge模式:
实现
产品的抽象 + 行为的分离(策略模式
)
总结
Bridge模式在我看来是对策略模式
的扩展,它的核心有两点:
- 只在一个维度上用继承,出现了多个维度则考虑分离并用组合,避免类的泛滥和抽象维度的混杂;
- 用
策略模式
隔离行为的变化,不要让行为/实现的变化污染原本的继承体系。
4. Decorator 装饰者
作用
有一系列产品,你希望动态地为他们添加额外 / 可自由组合 的功能,并且不影响产品本身。
Attach additional responsibilities to an object dynamically.
实现
Decorator 装饰器
继承产品抽象接口,并在内部持有一个产品(可能是具体产品,也有可能被装饰过了);Decorator
的具体实现,为其装饰的产品提供额外的功能,类似递归的调用;- 可以同时反复应用多个
Decorator
,实现额外功能的动态组合。
应用
1.
java的IO流的设计是一个典型的装饰者模式:
ByteArrayInputStream | FileInputStream | ObjectInputStream | StringBufferInputStream
是具体的输入流产品,根据数据来源区分;
FilterInputStream
是装饰器;
BufferedInputStream | DataInputStream | LineNumberInputStream | PushbackInputStream
是具体的装饰器,分别为其他输入流提供缓冲/类型读写/跟踪行号/退回已读数据的功能,这些装饰器是可以组合使用的:
InputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("test.txt")));
2.
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 模板方法
作用
将一个算法的通用骨架抽象到父类以避免代码重复,而将一些可变的步骤延迟到子类,子类不用关心算法结构,只需关注自己需要实现的特定步骤。
实现
这个没什么好说的。
四. 设计原则
- 单一职责
开放-关闭原则
一个类应当对扩展开放,对修改关闭。即当有新的需求时,不是修改已有的类,而是对已有的类进行扩展。
实现开闭原则的关键在于 分离不变和变化的部分,并对变化的部分进行合理的高层抽象,并让不变的部分依赖该高层抽象,这样就能在不同的实现间切换,或者扩展新的实现。很多设计模式都体现了这一点,比如策略模式
将算法抽象出来,模板方法
将不变的算法骨架与易变的需要自定义的步骤隔离,装饰者模式
将不变的核心功能对象和易变的增强功能隔离等等。里氏替换原则
子类必须能替换掉父类,这个原则通常由语言保证。依赖倒置
高层不直接依赖底层,而是高层定义自己需要底层提供什么样的接口,底层负责实现,这样就可以随意切换底层的具体实现而不用影响高层,但底层反而要依赖高层公布的接口,所以称为“依赖倒置”。接口分离原则 (interface segregation principle)
不应出现庞大的接口,迫使客户在使用时必须从一大堆它不需要的方法中寻找目的方法。这样的接口应该按照不同客户的需求被分离成若干小接口。最少知识原则(least knowledge principle)
类应当只与自己的朋友交互。该原则的思想是,将类对外部的了解尽量保持在一定范围内,尽量减少类之间的交互,从而降低各个组件间的耦合。
“朋友”的定义:- 当前对象的属性
- 当前对象所创建的对象
- 方法参数传递进来的参数
- 方法内创建的对象