设计模式 cheatsheet
设计模式,即Design Patterns,是指在软件设计中,被反复使用的一种代码设计经验。使用设计模式的目的是为了可重用代码,提高代码的可扩展性和可维护性。
设计模式这个术语是上个世纪90年代由Erich Gamma、Richard Helm、Raplh Johnson和Jonhn Vlissides四个人总结提炼出来的,并且写了一本Design Patterns
的书。这四人也被称为四人帮(GoF)。
为什么要使用设计模式?根本原因还是软件开发要实现可维护、可扩展,就必须尽量复用代码,并且降低代码的耦合度。设计模式主要是基于OOP编程提炼的,它基于以下几个原则:
- 开闭原则
由Bertrand Meyer提出的开闭原则(Open Closed Principle)是指,软件应该对扩展开放,而对修改关闭。这里的意思是在增加新功能的时候,能不改代码就尽量不要改,如果只增加代码就完成了新功能,那是最好的。
- 里氏替换原则
里氏替换原则是Barbara Liskov提出的,这是一种面向对象的设计原则,即如果我们调用一个父类的方法可以成功,那么替换成子类调用也应该完全可以运行。
抽象工厂 Abstract Factory
提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
不但工厂是抽象的,产品是抽象的,而且有多个产品需要创建,因此,这个抽象工厂会对应到多个实际工厂,每个实际工厂负责创建多个实际产品:
┌────────┐
─ >│ProductA│
┌────────┐ ┌─────────┐ │ └────────┘
│ Client │─ ─>│ Factory │─ ─
└────────┘ └─────────┘ │ ┌────────┐
▲ ─ >│ProductB│
┌───────┴───────┐ └────────┘
│ │
┌─────────┐ ┌─────────┐
│Factory1 │ │Factory2 │
└─────────┘ └─────────┘
│ ┌─────────┐ │ ┌─────────┐
─ >│ProductA1│ ─ >│ProductA2│
│ └─────────┘ │ └─────────┘
┌─────────┐ ┌─────────┐
└ ─>│ProductB1│ └ ─>│ProductB2│
└─────────┘ └─────────┘
适配器 Adapter
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
编写一个Adapter的步骤如下:
- 实现目标接口;
- 内部持有一个待转换接口的引用,这里是通过字段持有Callable接口;
- 在目标接口的实现方法内部,调用待转换接口的方法。
桥接 Bridge
将抽象与其实现分离,以便两者可以独立变化。
桥接模式实现比较复杂,实际应用也非常少,但它提供的设计思想值得借鉴,即不要过度使用继承,而是优先拆分某些部件,使用组合的方式来扩展功能。
生成器 Builder
将复杂对象的构造与其表示分开,以便相同的构造过程可以创建不同的表示。
责任链 Chain of Responsibility
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
┌─────────┐
│ Request │
└─────────┘
│
┌ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ┐
▼
│ ┌─────────────┐ │
│ ProcessorA │
│ └─────────────┘ │
│
│ ▼ │
┌─────────────┐
│ │ ProcessorB │ │
└─────────────┘
│ │ │
▼
│ ┌─────────────┐ │
│ ProcessorC │
│ └─────────────┘ │
│
└ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ┘
│
▼
命令 Command
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。
命令模式(Command)是指,把请求封装成一个命令,然后执行该命令。
┌──────┐ ┌───────┐
│Client│─ ─ ─>│Command│
└──────┘ └───────┘
│ ┌──────────────┐
├─>│ CopyCommand │
│ ├──────────────┤
│ │editor.copy() │─ ┐
│ └──────────────┘
│ │ ┌────────────┐
│ ┌──────────────┐ ─>│ TextEditor │
└─>│ PasteCommand │ │ └────────────┘
├──────────────┤
│editor.paste()│─ ┘
└──────────────┘
组合 Composite
将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
组合模式(Composite)经常用于树形结构,为了简化代码,使用Composite可以把一个叶子节点与一个父节点统一起来处理。
┌───────────┐
│ Node │
└───────────┘
▲
┌────────────┼────────────┐
│ │ │
┌───────────┐┌───────────┐┌───────────┐
│ElementNode││ TextNode ││CommentNode│
└───────────┘└───────────┘└───────────┘
装饰者 Decorator
动态地给一个对象添加一些额外的职责。就增加功能来说,相比生成子类更为灵活。
Decorator模式的目的就是把一个一个的附加功能,用Decorator的方式给一层一层地累加到原始数据源上,最终,通过组合加上重写接口获得我们想要的功能。
┌───────────┐
│ Component │
└───────────┘
▲
┌────────────┼─────────────────┐
│ │ │
┌───────────┐┌───────────┐ ┌───────────┐
│ComponentA ││ComponentB │... │ Decorator │
└───────────┘└───────────┘ └───────────┘
▲
┌──────┴──────┐
│ │
┌───────────┐ ┌───────────┐
│DecoratorA │ │DecoratorB │...
└───────────┘ └───────────┘
外观 Facade
为子系统中的一组接口提供一个一致的界面。Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
外观模式,即Facade,是一个比较简单的模式。它的基本思想如下:
如果客户端要跟许多子系统打交道,那么客户端需要了解各个子系统的接口,比较麻烦。如果有一个统一的“中介”,让客户端只跟中介打交道,中介再去跟各个子系统打交道,对客户端来说就比较简单。所以Facade就相当于搞了一个中介。
工厂方法 Factory Method
定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method使一个类的实例化延迟到其子类。
┌─────────────┐ ┌─────────────┐
│ Product │ │ Factory │
└─────────────┘ └─────────────┘
▲ ▲
│ │
┌─────────────┐ ┌─────────────┐
│ ProductImpl │<─ ─ ─│ FactoryImpl │
└─────────────┘ └─────────────┘
现实中使用更多的是静态工厂方法(Static Factory Method),例如:
Integer n = Integer.valueOf(100);
享元 Flyweight
运用共享技术有效地支持大量细粒度的对象。
如果一个对象实例一经创建就不可变,那么反复创建相同的实例就没有必要,直接向调用方返回一个共享的实例就行,这样即节省内存,又可以减少创建对象的过程,提高运行速度。
在java中,对于Byte来说,因为它一共只有256个状态,所以,通过Byte.valueOf()
创建的Byte实例,全部都是缓存对象。
因此,享元模式就是通过工厂方法创建对象,在工厂方法内部,很可能返回缓存的实例,而不是新创建实例,从而实现不可变实例的复用。
解释器 Interpreter
给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
常见的使用情况,如正则表达式,sql等。
迭代器 Iterator
提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
中介 Mediator
用一个中介对象来封装一系列的对象交互。中介者使各个对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
中介模式(Mediator)又称调停者模式,它的目的是把多方会谈变成双方会谈,从而实现多方的松耦合。
Mediator模式经常用在有众多交互组件的UI上。为了简化UI程序,MVC模式以及MVVM模式都可以看作是Mediator模式的扩展。
备忘录 Memento
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
主要用于捕获一个对象的内部状态,以便在将来的某个时候恢复此状态。Java的序列化也可以看作是备忘录模式。
观察者 Observer
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
观察者模式(Observer)又称发布-订阅模式(Publish-Subscribe:Pub/Sub)。它是一种通知机制,让发送通知的一方(被观察方)和接收通知的一方(观察者)能彼此分离,互不影响。
传统模式
┌─────────┐ ┌───────────────┐
│ Store │─ ─ ─>│ProductObserver│
└─────────┘ └───────────────┘
│ ▲
│
│ ┌─────┴─────┐
▼ │ │
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Product │ │ Admin │ │Customer │ ...
└─────────┘ └─────────┘ └─────────┘
广义的观察者模式包括所有消息系统。
所谓消息系统,就是把观察者和被观察者完全分离,通过消息系统本身来通知:
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
Messaging System
│ │
┌──────────────────┐
┌──┼>│Topic:newProduct │──┼─┐ ┌─────────┐
│ └──────────────────┘ ├───>│ConsumerA│
┌─────────┐ │ │ ┌──────────────────┐ │ │ └─────────┘
│Producer │───┼───>│Topic:priceChanged│────┘
└─────────┘ │ │ └──────────────────┘ │
│ ┌──────────────────┐ ┌─────────┐
└──┼>│Topic:soldOut │──┼─────>│ConsumerB│
└──────────────────┘ └─────────┘
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
原型 Prototype
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
原型模式,即Prototype
,是指创建新对象的时候,根据现有的一个原型来创建。
原型模式应用不是很广泛,因为很多实例会持有类似文件、Socket这样的资源,而这些资源是无法复制给另一个对象共享的,只有存储简单类型的“值”对象可以复制。
代理 Proxy
为其他对象提供一种代理以控制对这个对象的访问。
代理类似于Adapter
,通过组合实现目的,但是代理模式不会新建一个接口并实现,会直接实现原有的接口,在调用原方法前做一些额外的处置,达到一个类负责一件事的目的。
单例 Singleton
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
public static Singleton getInstance() {
return INSTANCE;
}
private Singleton() {}
}
需要异步加载,可以使用静态内部类(静态内部类调用时才会被加载)
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
状态 State
允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
状态模式(State)经常用在带有状态的对象中,目的是为了把上述一大串if...else...
的逻辑给分拆到不同的状态类中,使得将来增加状态比较容易。
策略 Strategy
定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。
策略模式的核心思想是在一个计算方法中把容易变化的算法抽出来作为“策略”参数传进去,从而使得新增策略不必修改原有逻辑。
模板方法 Template
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
模板方法(Template Method)是一个比较简单的模式。它的主要思想是,定义一个操作的一系列步骤,对于某些暂时确定不下来的步骤,就留给子类去实现好了,这样不同的子类就可以定义出不同的步骤。
模板方法的核心思想是:父类定义骨架,子类实现某些细节。为了防止子类重写父类的骨架方法,可以在父类中对骨架方法使用final。对于需要子类实现的抽象方法,一般声明为protected,使得这些方法对外部客户端不可见。
访问者 Visitor
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
访问者模式(Visitor)是一种操作一组对象的操作,它的目的是不改变对象的定义,但允许新增不同的访问者,来定义新的操作。
GoF原始的访问者模式,它是这么设计的:
┌─────────┐ ┌───────────────────────┐
│ Client │─ ─ ─ >│ Visitor │
└─────────┘ ├───────────────────────┤
│ │visitElementA(ElementA)│
│visitElementB(ElementB)│
│ └───────────────────────┘
▲
│ ┌───────┴───────┐
│ │
│ ┌─────────────┐ ┌─────────────┐
│ VisitorA │ │ VisitorB │
│ └─────────────┘ └─────────────┘
▼
┌───────────────┐ ┌───────────────┐
│ObjectStructure│─ ─ ─ ─>│ Element │
├───────────────┤ ├───────────────┤
│handle(Visitor)│ │accept(Visitor)│
└───────────────┘ └───────────────┘
▲
┌────────┴────────┐
│ │
┌───────────────┐ ┌───────────────┐
│ ElementA │ │ ElementB │
├───────────────┤ ├───────────────┤
│accept(Visitor)│ │accept(Visitor)│
│doA() │ │doB() │
└───────────────┘ └───────────────┘
上述模式的复杂之处在于上述访问者模式为了实现所谓的“双重分派”,过于复杂。简化的访问者模式,通过分离出访问者(处理逻辑),将其作为一个对象传入来处理逻辑。
访问者模式的核心思想是为了访问比较复杂的数据结构,不去改变数据结构,而是把对数据的操作抽象出来,在“访问”的过程中以回调形式在访问者中处理操作逻辑。