目录
一、GoF设计模式简介
1.GoF 的 23 种模式一览表
2.GoF 的 23 种设计模式的简要说明
3.抽象工厂模式
4.建造者模式
5.工厂方法模式
6.原型模式
7.单例模式
8.适配器模式
9.桥接模式
10.组合模式
11.装饰模式
12.外观模式
13.享元模式
14.代理模式
15.职责链模式
16.命令模式
17.解释器模式
18.迭代器模式
19.中介者模式
20.备忘录模式
21.观察者模式
22.状态模式
23.策略模式
24.模板方法模式
25.访问者模式
本节内容参考《Java设计模式》@刘伟 编著,清华大学出版社出版,设计模式详解请参考原书。
范围/目的 | 创建型模式 | 结构型模式 | 行为型模式 |
---|---|---|---|
类模式 | 工厂方法模式 | (类)适配器模式 | 解释器模式 模板方法模式 |
对象模式 | 抽象工厂模式 建造者模式 原型模式 单例模式 |
(对象)适配器模式 桥接模式 组合模式 装饰模式 外观模式 享元模式 代理模式 |
职责链模式 命令模式 迭代器模式 中介者模式 备忘录模式 观察者模式 状态模式 策略模式 访问者模式 |
模式类别 | 模式名称 | 模式说明 | 学习难度 | 使用频率 |
---|---|---|---|---|
创建型模式 Creational Patterns |
抽象工厂模式 Abstract Factory Pattern |
提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类 |
★★★★☆ |
★★★★★ |
建造者模式 Builder Pattern |
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示 |
★★★★☆ |
★★☆☆☆ |
|
工厂方法模式 Factory Method Pattern |
定义一个用于创建对象的接口,但是让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类 |
★★☆☆☆ |
★★★★★ |
|
原型模式 Prototype Pattern |
使用原型实例指定待创建对象的类型,并且通过复制这个原型来创建新的对象 |
★★★☆☆ |
★★★☆☆ |
|
单例模式 Singleton Pattern |
确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例 |
★☆☆☆☆ |
★★★★☆ |
|
结构型模式 Structural Patterns |
适配器模式 Adapter Pattern |
将一个类的接口转换成客户希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作 |
★★☆☆☆ |
★★★★☆ |
桥接模式 Bridge Pattern |
将抽象部分它的实现部分解耦,使得两者都能够独立变化 |
★★★☆☆ |
★★★☆☆ |
|
组合模式 Composite Pattern |
组合多个对象形成树形结构以表示具有部分-整体关系的层次结构。组合模式让客户端可以统一对待单个对象和组合对象 |
★★★☆☆ |
★★★★☆ |
|
装饰模式 Decorator Pattern |
动态地给一个对象增加一些额外的职责。就扩展功能而言,装饰模式提供了一种比使用子类更加灵活的替代方案 |
★★★☆☆ |
★★★☆☆ |
|
外观模式 Facade Pattern |
为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用 |
★☆☆☆☆ |
★★★★★ |
|
享元模式 Flyweight Pattern |
运用共享技术有效地支持大量细粒度对象的复用 |
★★★★☆ |
★☆☆☆☆ |
|
代理模式 Proxy Pattern |
给某一个对象提供一个代理或占位符,并由代理对象来控制对原来对象的访问 |
★★★☆☆ |
★★★★☆ |
|
行为型模式 Behavioral Patterns |
职责链模式 Chain of Responsibility Pattern |
避免将一个请求的发送者与接收者耦合在一起,让多个对象都有机会处理请求。将接收请求的对象连接成一条链,并且沿着这条链传递请求,直到有一个对象能够处理它为止 |
★★★☆☆ |
★★☆☆☆ |
命令模式 Command Pattern |
将一个请求封装为一个对象,从而可用不同的请求对客户进行参数化,对请求排队或者记录请求日志,以及支持可撤销的操作 |
★★★☆☆ |
★★★★☆ |
|
解释器模式 Interpreter Pattern |
给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子 |
★★★★★ |
★☆☆☆☆ |
|
迭代器模式 Iterator Pattern |
提供一种方法顺序访问一个聚合对象中的各个元素,而又不用暴露该对象的内部表示 |
★★★☆☆ |
★★★★★ |
|
中介者模式 Mediator Pattern |
定义一个对象来封装一系列对象的交互。中介者模式使各对象之间不需要显示地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互 |
★★★☆☆ |
★★☆☆☆ |
|
备忘录模式 Memento Pattern |
在不破坏封装的前提下捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态 |
★★☆☆☆ |
★★☆☆☆ |
|
观察者模式 Observer Pattern |
定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时其相关依赖对象皆得到通知并被自动更新 |
★★★☆☆ |
★★★★★ |
|
状态模式 State Pattern |
允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类 |
★★★☆☆ |
★★★☆☆ |
|
策略模式 Strategy Pattern |
定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法可以独立于使用它的客户而变化 |
★☆☆☆☆ |
★★★★☆ |
|
模板方法模式 Template Method Pattern |
定义一个操作中算法的框架,而将一些步骤延迟到子类中。模板方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤 |
★★☆☆☆ |
★★★☆☆ |
|
访问者模式 Visitor Pattern |
表示一个作用于某对象结构中的各个元素的操作。访问者模式可以在不改变各元素的类的前提下定义作用于这些元素的新操作 |
★★★★☆ |
★☆☆☆☆ |
抽象工厂模式是工厂方法模式的进一步延伸,由于它提供了功能更为强大的工厂类并且具备较好的可扩展性,在软件开发中得以广泛应用,尤其是在一些框架和API类库的设计中,例如在Java语言的AWT(抽象窗口工具包)中就使用了抽象工厂模式,它使用抽象工厂模式来实现在不同的操作系统中应用程序呈现与所在操作系统一致的外观界面。抽象工厂模式也是在软件开发中最常用的设计模式之一。
组成对象
结构 | 描述 |
---|---|
抽象工厂(AbstractFactory) | 它声明了一组用于创建一族产品的方法,每一个方法对应一种产品。 |
具体工厂(ConcreteFactory) | 它实现了在抽象工厂中声明的创建产品的方法,生成一组具体产品,这些产品构成了一个产品族,每一个产品都位于某个产品等级结构中。 |
抽象产品(AbstractProduct) | 它为每种产品声明接口,在抽象产品中声明了产品所具有的业务方法。 |
具体产品(ConcreteProduct) | 它定义具体工厂生产的具体产品对象,实现抽象产品接口中声明的业务方法。 |
场景分析
特性 | 描述 |
---|---|
优点 | 抽象工厂模式隔离了具体类的生成,使得客户端并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易,所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需要改变具体工厂的实例就可以在某种程度上改变整个软件系统的行为。 |
当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。 | |
增加新的产品族很方便,无须修改已有系统,符合开闭原则。 | |
缺点 | 增加新的产品等级结构麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不变,违背了开闭原则。 |
适用环境 | 一个系统不应该依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是很重要的,用户无须关心对象的创建过程,将对象的创建和适用解耦。 |
系统中有多于一个的产品族,而每次只使用其中某一产品族。可以通过配置文件等方式来使用户能够动态改变产品族,也可以很方便地增加新的产品族。 | |
属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。同一个产品族中的产品可以是没有任何关系的对象,但是它们都具有一些共同的约束,如同一操作系统下的按钮和文本框,按钮与文本框之间没有直接关系,但它们都属于某一操作系统的,此时具有一个共同的约束条件,及操作系统的类型。 | |
产品等级结构稳定,在设计完成之后不会向系统中增加新的产品等级结构或者删除已有的产品等级结构。 |
建造者模式的核心在于如何一步步构建一个包含多个组成部件的完整对象,使用相同的构建过程构建不同的产品。在软件开发中,如果需要创建复杂对象并希望系统具备很好的灵活性和扩展性可以考虑使用建造者模式。
组成对象
结构 | 描述 |
---|---|
抽象建造 (Builder) |
它为创建一个产品对象的各个部件指定抽象接口,在该接口中一般声明两类方法,一类方法是 buildPartX(),它们用于创建复杂对象的各个部件;另一类方法是 getResult(),它们用于返回复杂对象。Builder既可以是抽象类,也可以是接口。 |
具体建造者 (ConcreteBuilder) |
它实现了Builder接口,实现各个部件的具体构造和装配方法,定义并明确所创建的复杂对象,还可以提供一个方法返回创建好的复杂产品对象(该方法也可以由抽象建造者实现)。 |
产品 (Product) |
它是被构建的复杂对象,包含多个组成部件,具体建造者创建该产品的内部表示并定义它的装配过程。 |
指挥者 (Director) |
指挥者又称为导演类,他负责安排复杂对象的建造次序,指挥者与抽象建造者之间存在关联关系,可以在其 construct() 建造方法中调用建造者对象的部件构造与装配方法,完成复杂对象的建造。客户端一般只需要与指挥者进行交互,在客户端确定具体建造者的类型,并实例化具体建造者对象(也可以通过配置文件和反射机制实现),然后通过指挥者类的构造函数或者 Setter 方法将该对象传入指挥者类中。 |
场景分析
特性 | 描述 |
---|---|
优点 | 在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。 |
每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象。由于指挥者类针对抽象建造者编程,增加新的具体建造者无须修改原有类库的代码,系统扩展方便,符合开闭原则。 | |
可以更加精细地控制产品的创建过程。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。 | |
缺点 | 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,例如很多组成部分都不相同,不适合使用建造者模式,因此其使用范围受到一定的限制。 |
如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大,增加系统的理解难度和运行成本。 | |
适用环境 | 需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员变量。 |
需要生成的产品对象的属性相互依赖,需要指定其生成顺序。 | |
对象的创建过程独立于创建该对象的类。在建造者模式中通过引入指挥者类将创建过程封装在指挥者类中,而不在建造者类和客户类中。 | |
隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品。 |
工厂方法模式是简单工厂模式的延伸,它继承了简单工厂模式的优点,同时还弥补了简单工厂模式的不足。工厂方法模式是使用频率最高的设计模式之一,很多开源框架和API类库的核心模式。
组成对象
结构 | 描述 |
---|---|
抽象工厂(Factory) | 在抽象工厂类中声明了工厂方法,用于返回一个产品。抽象工厂是工厂方法模式的核心,所有创建对象的工厂类都必须实现该接口。 |
具体工厂(ConcreteFactory) | 它是抽象工厂类的子类,实现了在抽象工厂中声明的工厂方法,并可由客户端调用,返回一个具体产品类的实例。 |
抽象产品(Product) | 它是定义产品的接口,是工厂方法模式所创建对象的超类型,也就是产品对象的公共父类。 |
具体产品(ConcreteProduct) | 它实现了抽象产品接口,某种类型的具体产品由专门的具体工厂创建,具体工厂和具体产品之间一一对应。 |
场景分析
特性 | 描述 |
---|---|
优点 | 在工厂方法模式中,工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节,用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。 |
基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够让工厂自主确定创建何种产品对象,而如何创建这个对象的细节完全封装在具体工厂内部。工厂方法模式之所以又被称为多态工厂模式,正是因为所有的具体工厂类都具有同一抽象父类。 |
|
使用工厂方法模式的另一个优点是在系统中加入新产品时无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品即可,这样系统的可扩展性也就变得非常好,完全符合开闭原则。 |
|
缺点 | 在添加新产品时需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。 |
由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度。 |
|
适用环境 | 客户端不知道它所需要的的对象的类。在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体产品对象由具体工厂类创建,可将具体工厂类的类名存储在配置文件或数据库中。 |
抽象工厂类通过其子类来指定创建哪个对象。在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时子类对象将覆盖父类对象,从而使得系统更容易扩展。 |
原型模式作为一种快速创建大量相同或相似对象的方式,在软件开发中的应用较为广泛,很多软件提供的复制(Ctrl + C)和粘贴(Ctrl + V)操作就是原型模式的典型应用。
组成对象
结构 | 描述 |
---|---|
抽象原型类 (Prototype) |
它是声明克隆方法的接口,是所有具体原型类的公共父类,它可以是抽象类也可以是接口,甚至还可以是具体实现类。 |
客户类 (Client) |
在客户类中,让一个原型对象克隆自身从而创建一个新的对象,只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类 Prototype 编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。 |
具体原型类 (ConcretePrototype) |
它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。 |
场景分析
特性 | 描述 |
---|---|
优点 | 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。 |
扩展性较好,由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原类进行编程,而将具体原型类写在配置文件中,增加或减少产品类对原有系统没有任何影响。 |
|
原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构,而原型模式就不需要这样,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品。 |
|
可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(例如恢复到某一历史状态),可辅助实现撤销操作。 |
|
缺点 | 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时需要修改源代码,违背了开闭原则。 |
在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。 |
|
适用环境 | 创建新对象成本较大(例如初始化需要占用较长的时间,占用太多的CPU资源或网络资源),新对象可以通过复制已有对象来获得,如果是相似对象,则可以对其成员变量稍作修改。 |
系统要保存对象的状态,而对象的状态变化很小。 | |
需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。 |
单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中的使用频率相当高,在很多应用软件和框架中都得以广泛应用。
组成对象
结构 | 描述 |
---|---|
单例 (Singleton) |
在单例类的内部创建它的唯一实例,并通过静态方法 getInstance() 让客户端可以使用它的唯一实例;为了防止在外部对单例类实例化,将其构造函数的可见性设为 private;在单例类内部定义了一个 Singleton 类型的静态对象作为供外部共享访问的唯一实例。 |
场景分析
特性 | 描述 |
---|---|
优点 | 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。 |
由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。 |
|
允许可变数目的实例。基于单例模式可以进行扩展,使用与控制单例对象相似的方法来获得指定个数的实例对象,既节省系统资源,又解决了由于单例对象共享过多有损性能的问题。(注:自行提供指定数目实例对象的类可称为多例类。) |
|
缺点 | 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。 |
单例类的职责过重,在一定程度上违背了单一职责原则。因为单例类既提供了业务方法,又提供了创建对象的方法(工厂方法),将对象的创建和对象本身的功能耦合在一起。 |
|
现在很多面向对象语言(如 Java、C#)的运行环境都提供了自动来及回收技术,因此如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。 |
|
适用环境 | 系统只需要一个实例对象,例如系统要求提供一个唯一的序列号生成器或资源管理器,或者因为资源消耗太大而只允许创建一个对象。 |
客户调用类的单个实例只允许适用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。 |
适配器模式将现有接口转化为客户类所期望的接口,实现了对现有类的复用,它是一种使用频率非常高的设计模式,在软件开发中得到了广泛的应用。
组成对象
结构 | 描述 |
---|---|
目标抽象类 (Target) |
目标抽象类定义客户所需的接口,可以是一个抽象类或者接口,也可以是具体类。在类适配器中,由于 Java 语言不支持多重继承,他只能是接口。 |
适配器类 (Adapter) |
它可以调用另一个接口,作为一个转换器,对 Adaptee 和 Target 进行适配。适配器 Adapter是适配器模式的核心,在类适配器中,它通过实现关联一个 Adaptee 对象使二者产生联系。 |
适配者类 (Adaptee) |
适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配。适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下甚至没有适配者类的源代码。 |
场景分析
特性 | 结构 |
---|---|
优点通用 |
将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有构。 |
增加了类的头名性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用。 |
|
灵活性和扩展性都非常好,通过使用配置文件可以很方便的更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合开闭原则。 |
|
优点 类适配器 |
由于适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强。 |
优点 对象适配器 |
一个对象适配器可以把多个不同的适配者适配到同一个目标。 |
可以适配一个适配者的子类,由于适配器和适配者之间是关联关系,根据里氏代换原则,适配者的子类也可通过该适配器进行适配。 |
|
缺点 类适配器 |
对于 Java、C# 等不支持多重类继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配者。 |
适配者类不能为最终类,例如在 Java 中不能为 final 类。 | |
在 Java、C# 等语言中,类适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性。 |
|
缺点 对象适配器 |
与类适配器模式相比,在该模式下要在适配器中置换适配者类的某些方法比较麻烦。如果一定要置换掉适配器类的一个或多个方法,可以先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当成真正的适配者进行适配,实现过程较为复杂。 |
适用环境 | 系统需要使用一些现有的类,而这些类的接口(例如方法名)不符合系统的需要,甚至没有这些类的源代码。 |
想创建一个可以重复使用的类,用于和一些彼此之间没有太大关联的类(包括一些可能在将来引进的类)一起工作。 |
桥接模式是设计 Java 虚拟机和实现 JDBC 等驱动程序的核心模式之一,应用较为广泛。在软件开发中如果一个类或一个系统有多个变化维度都可以尝试使用桥接模式对其进行设计。桥接模式为多维度变化的系统提供了一套完整的解决方案,并且降低了系统的复杂度。
组成对象
结构 | 描述 |
---|---|
抽象类 (Abstraction) |
它是用于定义抽象类的接口,通常是抽象类而不是接口,其中定义了一个 Implementor(实现类接口)类型的对象并可以维护该对象,它与 Implementor 之间具有关联关系,它既可以包含抽象业务方法,也可以包含具体业务方法。 |
扩充抽象类 (RefinedAbstraction) |
它扩充由 Abstraction 定义的接口,通常情况下它不再是抽象类而是具体类,它实现了在 Abstraction 中声明的抽象业务方法,在 RefinedAbstraction 中可以调用在 Implementor 中定义的业务方法。 |
实现类接口 (Implementor) |
它是定义实现类的接口,这个接口不一定要与 Abstraction 的接口完全一致,事实上这两个接口可以完全不同。一般而言,Implementor 接口仅提供基本操作,而 Abstraction 定义的接口可能会做更多复杂的操作。Implementor 接口对这些基本操作进行了声明,而具体实现交给其子类。通过关联关系,在 Abstraction 中不仅拥有自己的方法,还可以调用到 Implementor 中定义的方法,使用关联关系来代替继承关系。 |
具体实现类 (ConcreteImplementor) |
它具体实现了 Implementor 接口,在不同的 ConcreteImplementor 中提供基本操作的不同实现,在程序运行时 ConcreteImplementor 对象将替换其父类对象,提供给抽象类具体的业务操作方法。 |
场景分析
特性 | 描述 |
---|---|
优点 |
分离抽象接口及其实现部分。桥接模式使用“对象间的关联关系”解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。所谓抽象和实现沿着各自维度变化,也就是说抽象和实现不在同一继承层次结构中,而是“子类化”它们,使它们各自具有自己的子类,以便任意组合子类,从而获得多维度组合对象。 |
在很多情况下,桥接模式可以取代多层继承方案,多层继承方案违背了单一职责原则,复用性较差,并且类的个数非常多,桥接模式是比多层继承方案更好的解决方法,它极大地觉少了子类的个数。 |
|
桥接模式提高了系统的可扩展性,在两个变化维度中任意扩展一个维度都不需要修改原有系统,符合开闭原则。 |
|
缺点 | 桥接模式的使用会增加系统的理解与设计难度,由于关联关系建立在抽象层,要求开发者一开始就针对抽象层进行设计与编程。 |
桥接模式要求正确地识别出系统中的两个独立变化的维度,因此其使用范围具有一定的局限性,如何正确识别两个独立维度也需要一定的经验积累。 |
|
适用环境 | 如果一个系统需要在抽象化和具体化之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系,通过桥接模式可以使它们在抽象层建立一个关联关系。 |
抽象部分和实现部分可以用继承的方式独立扩展而互不影响,在程序运行时可以动态地将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。 |
|
一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立进行扩展。 |
|
对于那些不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。 |
组合模式使用面向对象的思想来实现树形结构的构建与处理,描述了如何将容器对象和叶子对象进行递归组合,实现简单、灵活性好。由于在软件开发中存在大量的树形结构,因此组合模式是一种使用频率较高的结构型设计模式。
组成对象
结构 | 描述 |
---|---|
抽象构件 (Component) |
它可以是接口或抽象类,为叶子构件和容器构件对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现。在抽象构件中定义了访问及管理它们的子构件的方法,如增加子构件、删除子构件,获取子构件等。 |
叶子构件 (Leaf) |
它在组合结构中表示叶子节点对象,叶子节点没有子节点,它实现了在抽象构件中定义的行为。对于那些访问及管理子构件的方法,可以通过抛出异常、提示错误等方式进行处理。 |
容器构件 (Composite) |
它在组合结构中表示容器节点对象,容器节点包含子节点,其子节点可以是叶子结点,也可以是容器节点,它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括那些访问及管理子构件的方法,在其业务方法中可以递归调用其子结点的业务方法。 |
场景分析
特性 | 描述 |
---|---|
优点 | 可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。 |
客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。 |
|
在组合模式中增加新的容器构件和叶子构件都很方便,无须对现有类库进行任何修改,符合开闭原则。 |
|
为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子对象和容器对象的递归组合可以形成复杂的树形结构,但对树形结构的控制却非常简单。 |
|
缺点 | 在增加新构件时很难对容器中的构件类型进行限制。有时候希望一个容器中只能有某些特定类型的对象,例如在某个文件夹中只能包含文本文件,在使用组合模式时不能依赖类型系统来施加这些约束,因为它们都来自于相同的抽象层,在这种情况下必须通过在运行时进行类型检查来实现,这个实现过程较为复杂。 |
适用环境 | 在具有整体和部分的层次结构中希望通过一种方式忽略整体与部分的差异,客户端可以一致地对待它们。 |
在一个使用面向对象语言开发的系统中需要处理一个树形结构。 | |
在一个系统中能够分离出叶子对象和容器对象,而且它们的类型不固定,需要增加一些新的类型。 |
装饰模式降低了系统的耦合度,可以动态增加或删除对象的职责,并使需要装饰的具体构件类和具体装饰类可以独立变化,以便增加新的具体构件类和具体装饰类。使用装饰模式将大大减少子类的个数,让系统扩展起来比较方便,而且更容易维护,是取代继承复用的有效方式之一。在软件开发中装饰模式的应用较为广泛,例如在 Java I/O 中的输入流和输出流的设计、javax.swing 包中一些图形界面构件功能的增强等地方都运用了装饰模式。
组成对象
结构 | 描述 |
---|---|
抽象构件 (Component) |
它是具体构件和抽象装饰类的共同父类,声明了在具体构件中实现业务的方法,它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。 |
具体构建 (ConcreteComponent) |
它是抽象构件类的子类,用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰类可以给它增加额外的职责(方法)。 |
抽象装饰类 (Decorator) |
它也是抽象构件类的子类,用于给具体构件增加职责,但是具体职责在其子类中实现。它维护一个指向抽象构件对象的引用,通过该引用可以调用装饰之前的构件对象的方法,并通过其子类扩展该方法,以达到装饰的目的。 |
具体装饰类 (ConcreteDecorator) |
它是抽象装饰类的子类,负责向构件添加新的职责。每一个具体装饰类都定义了一些新的行为,他可以调用在抽象装饰中定义的方法,并可以增加新地方法用于扩充对象的行为。 |
场景分析
特性 | 描述 |
---|---|
优点 | 对于扩展一个对象的功能,装饰模式比继承更加灵活,不会导致类的个数急剧增加。 |
可以通过一种动态地方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的具体装饰类,从而实现不同的行为。 |
|
可以对一个对象进行多次装饰,通过使用不同的具体装饰类以及这些装饰类的排列组合可以创造出很多不同行为的组合,得到功能更加强大的对象。 |
|
具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构建类和具体装饰类,原有的类库代码无须改变,符合开闭原则。 |
|
缺点 | 在使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,大量小对象的产生势必会占用更多的系统资源,在一定程度上影响了程序的性能。 |
装饰模式提供了一种比继承更加灵活、机动的解决方案,但同时也意味着比继承更加易于出错,排错也更困难,对于多次装饰的对象,在调试是寻找错误可能需要逐级排查,较为繁琐。 |
|
适用环境 | 在不影响其他对象的情况下以动态、透明的方式给单个对象添加职责。 |
当不能采用继承的方式对系统进行扩展或采用继承不利于系统扩展和维护时可以使用装饰模式。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类已定义为不能被继承(例如在 Java 语言中使用final 关键字修饰的类)。 |
外观模式是一种使用频率非常高的设计模式,它通过引入一个外观角色来简化客户端与子系统之间的交互,为复杂的子系统调用提供一个统一的入口,使子系统与客户端的耦合度降低,且客户端调用非常方便。外观模式并不给系统增加任何新功能,它仅仅是简化调用接口。在几乎所有的软件中都能够找到外观模式的应用,例如绝大多数 B/S 系统都有一个首页或者导航页面,大部分 C/S 系统都提供了菜单或者工具栏,在这里首页和导航页面就是 B/S 系统的外观角色,而菜单和工具栏就是 C/S 系统的外观角色,通过它们用户可以快速访问子系统,降低了系统的复杂程度。所涉及与多个业务对象交互的场景都可以考虑使用外观模式进行重构,例如 Java EE 中的 Session 外观模式。
组成对象
结构 | 描述 |
---|---|
外观角色 (Facade) |
在客户端可以调用它的方法,在外观角色中可以知道相关的(一个或者多个)子系统的功能和责任;在正常情况下,它将所有从客户端发来的请求委派到相应的子系统,传递给相应的子系统对象处理。 |
子系统角色 (SubSystem) |
在软件系统中可以有一个或者多个子系统角色,每一个子系统可以不是一个单独的类,而是一个类的集合,它实现子系统的功能;每一个子系统都可以被客户端直接调用,或者被外观角色调用,它处理由外观类传过来的请求;子系统并不知道外观的存在,对于子系统而言,外观角色仅仅是另外一个客户端而已。 |
场景分析
特性 | 描述 |
---|---|
优点 | 它对于客户端屏蔽了子系统组件,减少了客户端所需处理的对象数目,并使子系统使用起来更加容易。通过引入外观模式,客户端代码将变得很简单,与之关联的对象也很少。 |
它实现了子系统与客户端之间的松耦合关系,这使得子系统的变化不会影响到调用它的客户端,只需要调整外观类即可。 |
|
一个子系统的修改对其他子系统没有任何影响,而且子系统内部变化也不会影响到外观对象。 |
|
缺点 | 不能很好地限制客户端直接使用子系统类,如果客户端访问子系统类做太多的限制则减少了可变性和灵活性。 |
如果设计不当,增加新的子系统可能需要修改外观类的源代码,违背了开闭原则。 | |
适用环境 | 当要为访问一系列复杂的子系统提供一个简单入口时可以使用外观模式。 |
客户端程序与多个子系统之间存在很大的依赖性。引入外观类可以将子系统与客户端解耦,从而提高子系统的独立性和可移植性。 |
|
在层次化结构中可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度。 |
当系统中存在大量相同或者相似的对象时享元模式是一种较好的解决方案,它通过共享技术实现相同或相似的细粒度对象的复用,从而节约了内存空间,提高了系统性能。相比其他结构型设计模式,享元模式的使用频率并不算太高,但是作为一种以“节约内存,提高性能”为出发点的设计模式,它在软件开发中还是得到了一定程度的应用。
组成对象
结构 | 描述 |
---|---|
抽象享元类 (Flyweight) |
抽象享元类通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。 |
具体享元类 (ConcreteFlyweight) |
具体享元类实现了抽象享元类,其实例称为享元对象;在具体享元类中为内部状态提供了存储空间。通常可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。 |
非共享具体享元类 (UnsharedConcreteFlyweight) |
并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。 |
享元工厂类 (FlyweightFactory) |
享元工厂类用于创建并管理享元对象,它针对抽象享元类编程,将各种类型的具体享元对象存储在一个享元池中,享元池一般设计为一个存储“键值对”的集合(也可以是其他类型的集合),可以结合工厂模式进行设计;当用户请求一个具体享元对象时,享元工厂提供一个存储在享元池中已创建的实例或者创建一个新的实例(如果不存在),返回新创建的实例并将其存储在享元池中。 |
场景分析
特性 | 描述 |
---|---|
优点 | 享元模式可以减少内存中对象的数量,使得相同或者相似对象再内存中只保存一份。从而可以节约系统资源,提高系统性能。 |
享元模式的外部状态相对孤立,而且不会影响其内部状态,从而使享元对象可以在不同的环境中被共享。 |
|
缺点 | 享元模式使系统变得复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。 |
为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态将使运行时间变长。 |
|
适用环境 | 一个系统有大量相同或者相似的对象,造成内存的大量耗费。 |
对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。 | |
在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此应当在需要多次重复使用享元对象时才使用享元模式。 |
代理模式是常用的结构型设计模式之一,它为对象的间接访问提供了一个解决方案,可以对对象的访问进行控制。代理模式的类型较多,其中远程代理、虚拟代理、保护代理等在软件开发中的应用非常广泛。在 Java RMI、EJB、Web Service、Spring AOP 等技术和框架中都使用了代理模式。
组成对象
结构 | 描述 |
---|---|
抽象主题角色 (Subject) |
它声明了真实主题和代理主题的共同接口,这样一来在任何使用真实主题的地方都可以使用代理主题,客户端通常需要针对抽象主题角色进行编程。 |
代理主题角色 (Proxy) |
它包含了对真实主题的引用,从而可以在任何时候操作真实主题对象;在代理主题角色中提供了一个与真实主题角色相同的接口,以便在任何时候都可以替代真实主题;代理主题角色还可以控制对真实主题的使用,负责在需要的时候创建和删除真实主题对象,并对真实主题对象的使用加以约束。通常,在代理主题角色中客户端在调用所引用的真实主题操作之前或之后还需要执行其他操作,而不仅仅是单纯调用真实主题对象中的操作。 |
真实主题角色 (RealSubject) |
它定义了代理角色所代表的真实对象,在真实主题角色中实现了真实的业务操作,客户端可以通过代理主题角色间接调用真实主题角色中定义的操作。 |
场景分析
特性 | 描述 |
---|---|
优点 | 能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。 |
客户端可以针对抽象主题角色进行编程,增加和更换代理类无须修改源代码,符合开闭原则,系统具体较好的灵活性和可扩展性。 |
|
优点 远程代理 |
远程代理为位于两个不同地址空间的对象的访问提供了一种实现机制,可以将一些消耗资源较多的对象和操作移至性能更好的计算机上,提高了系统的整体运行效率。 |
优点 虚拟代理 |
虚拟代理通过一个消耗资源较少的对象来代表一个消耗资源较多的对象,可以在一定程度上节省系统的运行开销。 |
优点 缓冲代理 |
缓冲代理为某一个操作的结果提供临时的缓存存储空间,以便在后续使用中能够共享这些结果,优化系统性能,缩短执行时间。 |
优点 保护代理 |
保护代理可以控制对一个对象的访问权限,为不同用户提供不同级别的使用权限。 |
缺点 | 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢,例如保护代理。 |
实现代理模式需要额外的工作,而且有些代理模式的实现过程较为复杂,例如远程代理。 |
|
适用环境 | 当客户端对象需要访问远程主机中的对象时可以使用远程代理。 |
当需要用一个消耗资源较少的对象来表示一个消耗资源较多的对象,从而降低系统开销、缩短运行时间时可以使用虚拟代理,例如一个对象需要很长时间才能完成加载时。 |
|
当需要为某一个被频繁访问的操作结果提供一个临时存储空间,以供多个客户端共享访问这些结果时可以使用缓冲代理。通过使用缓冲代理,系统无须再客户端每一次访问时都重新执行操作,只需要直接从临时缓冲区获取操作结果即可。 |
|
当需要控制对一个对象的访问为不同用户提供不同级别的访问权限时可以使用保护代理。 |
|
当需要为一个对象的访问(引用)提供一些额外的操作时可以使用智能引用代理。 |
职责链模式通过建立一条链来组织请求的处理者,请求将沿着链进行传递,请求发送者无须知道请求在何时、何处以及如何被处理,实现了请求发送者与处理者的解耦。在软件开发中,如果遇到有多个对象可以处理同一请求可以应用职责链模式,例如在 Web 应用开发中创建多个过滤器(Filter)链来对请求数据进行过滤,在工作流系统中实现公文的分级审批等,使用职责链模式可以较好地解决此类问题。Java 语言中的异常处理(Exception Handlers)机制也是职责链模式的典型应用之一,不同的catch 子句可以处理不同类型的异常,这些 catch 子句构成了一条处理异常对象的职责链。
组成对象
结构 | 描述 |
---|---|
抽象处理者 (Handler) |
它定义了一个处理请求的接口,一般设计为抽象类,由于不同的具体处理者处理请求的方式不同,因此在其中定义了抽象请求处理方法。每一个处理者的下家还是一个处理者,故在抽象处理者中定义了一个抽象处理者类型的对象作为其对下家的引用,通过该引用处理者可以连成一条链。 |
具体处理着 (ConcreteHandler) |
它是抽象处理者的子类可以处理用户请求,在具体处理者类中实现了抽象处理者中定义的抽象请求处理方法,在处理请求之前需要进行判断,看是否有相应的处理权限,如果可以处理请求就处理它,否则将请求转发给后继者;在具体处理者中可以访问链中的下一个对象,以便请求转发。 |
场景分析
特性 | 描述 |
---|---|
优点 | 职责链模式使得一个对象无须知道其他哪一个对象处理其请求,对象仅需知道该请求会被处理即可,接收者和发送者都没有对方的明确信息,并且链中的对象不需要知道链的结构,由客户端负责链的创建,降低了系统的耦合度。 |
请求处理对象仅需维持一个指向其后继者的引用,而不需要维持它对所有的候选处理者的引用,可简化对象之间的相互连接。 |
|
在给对象分配职责时,职责链可以带来更多的灵活性,可以通过在运行时对该链进行动态地增加或修改来增加或改变处理一个请求的职责。 |
|
在系统中增加一个新的具体请求处理者时无须修改原有系统的代码,只需要在客户端重新建链即可,从这一点来看是符合开闭原则的。 |
|
缺点 | 由于一个请求没有明确的接收者,那么就不能保证它一定会被处理,该请求可能一直到链的末端都得不到处理;一个请求也可能因职责链没有被正确配置而得不到处理。 |
对于比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定的影响,而且在进行代码调试时不太方便。 |
|
如果建链不当,可能会造成循环调用,将导致系统陷入死循环。 | |
适用环境 | 有多个对象可以处理同一个请求,具体哪个对象处理该请求待运行时刻再确定,客户端只需将请求提交到链上,而无须关心请求的处理对象是谁以及它是如何处理的。 |
在不明确指定接收者的情况下向多个对象中的一个提交一个请求。 | |
可动态指定一组对象处理请求,客户端可以动态创建职责链来处理请求,还可以改变链中处理者之间的先后次序。 |
命令模式是一种使用频率非常高的设计模式,它可以将请求发送者与接收者解耦,请求发送者通过命令对象来间接引用请求接收者,使得系统具有更好的灵活性和可扩展性。在基于 GUI 的软件开发(无论是电脑桌面应用还是手机移动应用)中,命令模式都得到了广泛的应用。
组成对象
结构 | 描述 |
---|---|
抽象命令类 (Command) |
抽象命令类一般是一个抽象类或接口,在其中声明了用于执行请求的 execute() 等方法,通过这些方法可以调用请求接收者的相关操作。 |
具体命令类 (ConcreteCommand) |
具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中。具体命令类在实现 execute() 方法时将调用接收者对象的相关操作(Action)。 |
调用者 (Invoker) |
调用者即请求发送者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类之间存在关联关系。在程序运行时可以将一个具体命令对象注入其中,再调用具体命令对象的 execute() 方法,从而实现间接调用请求接收者的相关操作。 |
接收者 (Receiver) |
接收者执行与请求相关的操作,具体实现对请求的业务处理。 |
场景分析
特性 | 描述 |
---|---|
优点 | 降低系统的耦合度。由于请求者与接收者之间不存在直接引用,因此请求者与接收者之间实现完全解耦,相同的请求者可以对应不同的接收者,同样相同的接收者也可以供不同的请求者使用,两者之间具有良好的独立性。 |
新的命令可以很容易地加入到系统中。由于增加新的具体命令类不会影响到其他类,因此增加新的具体命令类很容易,无须修改原有系统源代码,甚至客户类代码,满足开闭原则的要求。 |
|
可以比较容易地设计一个命令队列或宏命令(组合命令)。 | |
为请求的撤销(Undo)和恢复(Redo)操作提供了一种设计和实现方案。 | |
缺点 | 使用命令模式可能会导致某些系统有过多的具体命令类,因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类,所以在某些系统中可能需要提供大量的具体命令类,者将影响命令模式的使用。 |
适用环境 | 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。请求调用者无须知道接收者的存在,也无须知道接收者是谁,接收者也无须关心何时被调用。 |
系统需要在不同的时间指定请求、将请求排队和执行请求。一个命令对象和请求的初始调用者可以有不同的生命期,换而言之,最初的请求发出者可能已经不存在,而命令对象本身仍然是活动的,可以通过该命令对象去调用请求接收者,并且无须关心请求调用者的存在性,可以通过请求日志文件等机制来具体实现。 |
|
系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。 | |
系统需要将一组操作组合在一起形成宏命令。 |
解释器模式为自定义语言的设计和实现提供了一种解决方案,用于定义一组文法规则并通过这组文法规则来解释语言中的句子。虽然解释器模式的使用频率不是特别高,但是它在正则表达式、XML 文档解释等领域还是得到了广泛使用。与解释器模式类似,目前还诞生了很多基于抽象语法树的源代码处理工具,例如 Eclipse 中的 Eclipse AST,它可以使用于表示和处理 Java 语言的语法结构,用户可以通过扩展其功能创建自己的文法规则,实现对源代码的分析。
组成对象
结构 | 描述 |
---|---|
抽象表达式 (AbstractExpression) |
在抽象表达式中声明了抽象的解释操作,它是所有终结符表达式和非终结符表达式的公共父类。 |
终结符表达式 (TerminalExpression) |
终结符表达式是抽象表达式的子类,它实现了与文法中的终结符相关联的解释操作,在句子中的每一个终结符否是该类的一个实例。通常在一个解释器模式中只有少数几个终结符表达式类,它们的实例可以通过非终结符表达式组成较为复杂的句子。 |
非终结符表达式 (NonterminalExpression) |
非终结符表达式也是抽象表达式的子类,它实现了文法中非终结符的解释操作,由于在非终结符表达式中可以包含终结符表达式,也可以继续包含非终结符表达式,因此其解释操作一般通过递归的方式完成。 |
环境类 (Context) |
环境类又称为上下文类,它用于存储解释器之外一些全局信息,通常它临时存储了需要解释的语句。 |
场景分析
特性 | 描述 |
---|---|
优点 | 易于改变和扩展文法。由于在解释器模式中使用类表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。 |
每一条文法规则都可以表示为一个类,因此可以方便的实现一个简单的语言。 | |
实现文法较为容易。在抽象语法树种每一个表达式节点类的实现方式都是相似的,这些累的代码编写不会特别复杂,还可以通过一些工具自动生成节点类代码。 |
|
增加新的解释器表达式较为方便。如果用户要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合开闭原则。 |
|
缺点 | 对于复杂文法难以维护。在解释器模式中每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护,此时可以考虑使用语法分析程序等方式来取代解释器模式。 |
执行效率较低。由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调式过程也比较麻烦。 |
|
适用环境 | 可以将一个需要解释执行的语言中的句子表示为一颗抽象语法树。 |
一些重复出现的问题可以用一种简单的语言进行表达。 | |
一个语言的文法较为简单。对于复杂的文法,解释器模式中的文法类层次结构将变得很庞大而无法管理,此时最好使用语法分析程序生成器。 |
|
执行效率不是关键问题。高效的解释器通常不是通过直接解释抽象语法树来实现的,而是需要将它们转换成其他形式,使用解释器模式的执行效率并不高。 |
迭代器模式是一种使用频率非常高的设计模式,通过引入迭代器可以将数据的便利功能从聚合对象中分离出来,聚合对象只负责存储数据,而遍历数据由迭代器来完成。由于很多编程语言的类库都已经实现了迭代器模式,因此在实际开发中只需要直接使用 Java、C# 等语言已定义好的迭代器即可,迭代器已经成为操作聚合对象的基本工具之一。
组成对象
结构 | 描述 |
---|---|
抽象迭代器 (Iterator) |
它定义了访问和遍历元素的接口,声明了用于遍历数据元素的方法,例如用于获取第一个元素的 first() 方法、用于访问下一个元素的 next() 方法、用于判断是否还有下一个元素的 hasNext() 方法、用于获取当前元素的 currentItem() 方法等,在具体迭代器中将实现这些方法。 |
具体迭代器 (ConcreteIterator) |
它实现了抽象迭代器接口,完成对聚合对象的遍历,同时在具体迭代器中通过游标来记录在聚合对象中所处的当前位置,在具体实现时游标通常是一个表示位置的非负整数。 |
抽象聚合类 (Aggregate) |
它用于存储和管理元素对象,声明一个 createIterator() 方法用于创建一个迭代器对象,充当抽象迭代器工厂角色。 |
具体聚合类 (ConcreteAggregate) |
它是抽象聚合类的子类,实现了在抽象聚合类中声明的 createIterator() 方法,该方法返回一个与该具体聚合类对应的具体迭代器 ConcreteIterator 实例。 |
场景分析
特性 | 描述 |
---|---|
优点 | 迭代器模式支持以不同的方式遍历一个聚合对象,在同一聚合对象上可以定义多种遍历方式。在迭代器模式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,也可以自己定义迭代器的子类以支持新的遍历方式。 |
迭代器模式简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方法,这样可以简化聚合类的设计。 |
|
在迭代器模式中引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改源代码,满足开闭原则。 |
|
缺点 | 由于迭代器模式将存储数据和遍历数据的职责分离,在增加新的聚合类时需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。 |
抽象迭代器的设计难度较大,需要充分考虑到系统将来的扩展。在自定义迭代器时创建一个考虑全面的抽象迭代器并不是一件很容易的事情。 |
|
适用环境 | 访问一个聚合对象的内容而无须暴露它的内部表示。将聚合对象的访问与内部数据的存储分离,使得访问聚合对象时无须了解其内部实现细节。 |
需要为一个聚合对象提供多种遍历方式。 | |
为遍历不同的聚合结构提供一个统一的接口,在该接口的实现类中为不同的聚合结构提供不同的遍历方式,而客户端可以一致性地操作该接口。 |
中介者模式将一个网状的系统结构变成一个以中介者对象为中心的星形结构,在这个星形结构中使用中介者对象与其他对象的一对多关系来取代原有对象之间的多对多关系。中介者模式在事件驱动类软件中的应用较为广泛,特别是基于 GUI(Graphical User Interface,图形用户界面)的应用软件。此外,在类与类之间存在错综复杂的关联关系的系统中,中介者模式也得到了较好的应用。
组成对象
结构 | 描述 |
---|---|
抽象中介者 (Mediator) |
它定义了一个接口,该接口用于与各同事对象之间进行通信。 |
具体中介者 (ConcreteMediator) |
它是抽象中介者的子类,通过协调各个同事对象来实现协作行为,他维持了对各个同事对象的引用。 |
抽象同事类 (Colleague) |
它定义了各个同事类公有的方法,并声明了一些抽象方法供子类实现,同时它维持了一个对抽象中介者类的引用,其子类可以通过该引用与中介者通信。 |
具体同事类 (ConcreteColleague) |
它是抽象同事类的子类,每一个同事对象在需要和其他同事对象通信时先与中介者通信,通过中介者间接完成与其他同事类的通信;在具体同事类中实现了在抽象同事勒种声明的抽象方法。 |
场景分析
特性 | 描述 |
---|---|
优点 | 中介者模式简化了对象之间的交互,它用中介者和同事的一对多交互替代了原来同事之间的多对多交互,一对多关系更容易理解、维护和扩展,将原本难以理解的网状结构转换成相对简单的星形结构。 |
可将各同事对象解耦。中介者有利于各同事之间的松耦合,可以独立地改变和复用每一个同事和中介者,增加新的中介者类和新的同事类都比较方便,更好地符合开闭原则。 |
|
可以减少子类生成,中介者将原本分布于多个对象间的行为集中在一起,改变这些行为只需生成新的中介者子类即可,这使得各个同事类可以被重用,无须直接对同事类进行扩展。 |
|
缺点 | 在具体中介者类中包含了大量同事之间的交互细节,可能会导致具体中介者类非常复杂,使得系统难以维护。 |
适用环境 | 系统中对象之间存在复杂的引用关系,系统结构混乱且难以理解。 |
一个对象由于引用了其他很多对象并且直接和这些对象通信,导致难以复用该对象。 |
|
想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类,此时可以通过引入中介者类来实现,在中介者中定义对象交互的公共行为,如果需要改变行为则可以增加新的具体中介者类。 |
备忘录模式在很多软件的使用过程中普遍存在,但是在应用软件开发中它的使用频率并不太高,因为现在很多基于窗体和浏览器的应用软件并没有提供撤销操作。如果需要为软件提供撤销功能,备忘录模式无疑是一种很好的解决方案。在一些字处理软件,图像编辑软件、数据库管理系统等软件中备忘录模式都得到了很好的应用。
组成对象
结构 | 描述 |
---|---|
原发器 (Originator) |
原发器是一个普通类,他通过创建一个备忘录来存储当前内部状态,也可以使用备忘录来恢复其内部状态,一般将系统中需要保存内部状态的类设计为原发器。 |
备忘录 (Memento) |
备忘录用于存储原发器的内部状态,根据原发器来决定保存哪些内部状态。备忘录的设计一般可以参考原发器的设计,根据实际需要确定备忘录类中的属性。需要注意的是,除了原发器本身与负责人类之外,备忘录对象不能直接供其他类使用,原发器的设计在不同的编程语言中实现机制会有所不同。 |
负责人 (Caretaker) |
负责人又称管理者,他负责保存备忘录,但是不能对备忘录的内容进行操作或检查。在负责人类中可以存储一个或多个备忘录对象,它只负责存储对象,而不能修改对象,也无须知道对象的实现细节。 |
场景分析
特性 | 描述 |
---|---|
优点 | 提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时可以使用暂时存储起来的备忘录将状态复原。 |
备忘录实现了对信息的封装,一个备忘录对象是一种原发器对象状态的表示,不会被其他代码所改动。备忘录保存了原发器的状态,采用列表、堆栈等集合来存储备忘录对象可以实现多次撤销操作。 |
|
缺点 | 资源消耗过大,如果需要保存的原发器类的成员变量太多,就不可避免地需要占用大量的存储空间,每保存一次对象的状态都需要消耗一定的系统资源。 |
适用环境 | 保存一个对象在某一个时刻的全部状态或部分状态,这样以后需要时它能够恢复到先前的状态,实现撤销操作。 |
防止外界对象破坏一个对象历史状态的封装性,避免将对象历史状态的实现细节暴露给外界对象。 |
观察者模式是一种使用频率非常高的设计模式,无论是移动应用、Web 应用或者桌面应用,观察者模式几乎无处不在,它为实现对象之间的联动提供了一套完整的解决方案,凡是涉及一对一或者一对多的对象交互场景都可以使用观察者模式。观察者模式广泛应用于各种编程语言的 GUI 事件处理的实现,在基于事件的 XML 解析技术(例如 SAX2)以及 Web 事件处理中也都使用了观察者模式。
组成对象
结构 | 描述 |
---|---|
目标 (Subject) |
目标又称为主题,它是指被观察的对象。在目标中定义了一个观察者集合,一个观察目标可以接受任意数量的观察者来观察,它提供了一系列方法来在增加和删除观察者对象,同时它定义了通知方法 notify() 。目标类可以是接口,也可以是抽象类或具体类。 |
具体目标 (ConcreteSubject) |
具体目标是目标类的子类,它通常包含有经常发生改变的数据,当它的状态发生改变时将向它的各个观察者发出通知;同时它还实现了在目标类中定义的抽象业务逻辑方法(如果有)。如果无须扩展目标类,则具体目标类可以省略。 |
观察者 (Observer) |
察者将对观察目标的改变做出反应,观察者一般定义为接口,该接口声明了更新数据的方法 update() ,因此又称为抽象观察者。 |
具体观察者 (ConcreteObserver) |
在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者 Observer 中定义的 update() 方法。通常实现时可以调用具体目标类 attach() 方法将自己添加到目标类的集合中或通过 detach() 方法将自己从目标类的集合中删除。 |
场景分析
特性 | 描述 |
---|---|
优点 | |
在观察目标和观察者之间建立一个抽象的耦合。观察目标只需要维持一个抽象观察者的集合,无须了解其具体观察者。由于观察目标和观察者没有紧密地耦合在一起,因此它们可以属于不同的抽象化层次。 |
|
支持广播通信,观察目标会向所有已注册的观察者对象发送通知,简化了一对多系统设计的难度。 |
|
符合开闭原则,增加新的具体观察者无须修改原有系统代码,在具体观察者与观察目标之间不存在关联关系的情况下增加新的观察目标也很方便。 |
|
缺点 | 如果一个观察目标对象有很多直接和间接观察者,将所有的观察者都通知到会花费很多时间。 |
如果在观察者和观察目标之间存在循环依赖,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。 |
|
观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。 |
|
适用环境 | 一个抽象模型有两个方面,其中一个方面依赖于另一个方面,将这两个方面封装在独立地对象中使它们可以各自独立地改变和复用。 |
一个对象的改变将导致一个或多个其他对象也发生改变,而并不知道具体有多少对象将发生改变,也不知道这些对象是谁。 |
|
需要在系统中创建一个触发链,A 对象的行为将影响B对象,B 对象的行为将影响 C 对象......,可以使用观察者模式创建一种链式触发机制。 |
状态模式将一个对象在不同状态下的不同行为封装在一个个状态类中,通过设置不同的状态对象可以让环境对象拥有不同的行为,而状态转换的细节对于客户端而言是透明的,方便了客户端的使用。在实际开发中,状态模式具有较高的使用频率,在工作流、游戏等软件中状态模式都得到了广泛的应用,例如公文状态的转换、游戏中角色的升级等。
组成对象
结构 | 描述 |
---|---|
环境类 (Context) |
环境类又称为上下文类,它是拥有多种状态的对象。由于环境类的状态存在多样性且在不同状态下对象的行为有所不同,因此将状态独立出去形成单独的状态类。在环境类中维护了一个抽象状态类 State 的实例,这个实例定义当前状态,在具体实现时它是一个 State 子类的对象。 |
抽象状态类 (State) |
它用于定义一个接口以封装与环境类的一个特定状态相关的行为,在抽象状态类中声明了各种不同状态对应的方法,而在其子类中实现了这些方法,由于不同状态下对象的行为可能不同,因此在不同的子类中方法的实现可能存在不同,相同的方法可以卸载抽象状态类中。 |
具体状态类 (ConcreteState) |
它是抽象状态类的子类,每一个子类实现一个与环境类的一个状态相关的行为,每一个具体状态类对应环境的一个具体状态,不同的具体状态类的行为有所不同。 |
场景分析
特性 | 描述 |
---|---|
优点 | 状态模式封装了状态的转换与规则,在状态模式中可以将状态的转换代码封装在环境类或者具体状态类中,可以对状态转换代码进行集中管理,而不是分散坐在一个个业务方法中。 |
状态模式将所有与某个状态有关的行为放到一个类中,只需要注入一个不同的状态对象即可使环境对象拥有不同的行为。 |
|
状态模式允许状态转换逻辑与状态对象合成一体,而不是提供一个巨大的条件语句块,状态模式可以避免使用庞大的条件语句将业务方法和状态转换代码交织在一起。 |
|
状态模式可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。 | |
缺点 | 状态模式会增加系统中类和对象的个数,导致系统运行开销增大。 |
状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱,增加系统设计的难度。 |
|
状态模式对开闭原则的支持并不太好,增加新的状态类需要修改那些负责状态转换的源代码,否则无法转换到新增的状态;而且修改某个状态类的行为也需要修改对应类的源代码。 |
|
适用环境 | 对象的行为依赖于它的状态(例如某些属性值),状态的改变将导致行为的变化。 |
在代码中包含大量与对象状态有关的条件语句,这些条件语句的出现会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,并且导致客户类与类库之间的耦合增强。 |
策略模式用于算法的自由切换和扩展,它是应用较为广泛的设计模式之一。策略模式对应于解决某一个问题的一个算法族,允许用户来解决某一问题,同时可以方便地更换算法或者增加新的算法,只要涉及算法的封装、复用和切换都可以考虑使用策略模式。
组成对象
结构 | 描述 |
---|---|
环境类 (Context) |
环境类是使用算法的角色,它在解决某个问题(即实现某个功能)时可以采用多种策略。在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略。 |
抽象策略类 (Strategy) |
抽象策略类为所支持的算法声明了抽象方法,是所有策略类的父类,它可以是抽象类或具体类,也可以是接口。环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法。 |
具体策略类 (ConcreteStrategy) |
具体策略类实现了在抽象策略类中声明的算法,在运行时具体策略类将覆盖在环境类中定义的抽象策略类对象,使用一种具体的算法实现某个业务功能。 |
场景分析
特性 | 描述 |
---|---|
优点 | 策略模式提供了对开闭原则的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。 |
策略模式提供了管理相关的算法族的办法。策略模式的等级结构定义了一个算法或行为族,恰当地使用继承可以把公共的代码移到抽象策略类中,从而避免重复的代码。 |
|
策略模式提供了一种可以替换继承关系的办法,如果不使用策略模式,那么使用算法的环境类就可能会有一些子类,每一个子类提供一种不同的算法。但是这样一来算法的使用就和算法本身混在一起,不符合单一职责原则,决定使用哪一种算法的逻辑和该算法本身混合在一起,从而不可能再独立演化;而使用继承无法实现算法或行为在程序运行时的动态切换。 |
|
使用策略模式可以避免多重条件选择语句。多重条件选择语句不易维护,他把采取哪一种算法或行为的逻辑与算法或行为本身的实现逻辑混合在一起,将它们全部硬编码在一个庞大的多重条件选择语句中,比直接继承环境类的办法还要原始和落后。 |
|
策略模式提供了一种算法的复用机制,由于将算法单独提取出来封装在策略模式类中,因此不同的环境类可以方便地复用这些策略类。 |
|
缺点 | 客户端必须知道所有的策略类,并自行决定使用哪一个策略类,这就意味着客户端必须理解这些算法的区别,以便适当时选择恰当的算法。换而言之,策略模式只适用于客户端知道所有算法或行为的情况。 |
策略模式将造成系统产生很多具体策略类,任何细小的变化都将导致系统要增加一个新的具体策略类。 |
|
无法同时在客户端使用多个策略类,也就是说,在使用策略模式时客户端每次只能使用一个策略类,不支持使用一个策略类完成部分功能后再使用另一个策略类完成剩余功能的情况。 |
|
适用环境 | 一个系统需要动态地在几种算法中选择一种,那么可以将这些算法封装到一个个的具体算法类中,而这些具体算法类都是一个抽象算法类的子类。换而言之,这些具体算法类均有统一的接口,根据里氏代换原则和面向对象的多态性,客户端可以选择使用任何一个具体算法类,并只需要维持一个数据类型是抽象算法类的对象。 |
一个对象有很多行为,如果不用恰当地模式,这些行为则只好使用多重条件选择语句来实现。此时使用策略模式把这些行为转移到相应的具体策略类里面,就可以避免使用难以维护的多重条件选择语句。 |
|
不希望客户端知道复杂的、与算法相关的数据结构,在具体策略类中封装算法与相关的数据结构,可以提高算法的保密性与安全性。 |
模板方法模式是基于继承的代码复用技术,它体现了面向对象的诸多重要思想,是一种使用较为频繁的设计模式。模板方法模式广泛应用于框架设计(例如 Spring、JUnit 等)中,以确保通过父类来控制处理流程的逻辑程序(例如框架的初始化、测试流程的设置等)。
组成对象
结构 | 描述 |
---|---|
抽象类 (AbstractClass) |
在抽象类中定义了一系列基本操作(Primitive Operations),这些基本操作可以是具体的,也可以是抽象的,每一个基本操作对应算法的一个步骤,在其子类中可以重定义或实现这些步骤。同时在抽象类中实现了一个模板方法(Template Method),用于定义一个算法的框架,模板方法不仅可以调用在抽象类中实现的基本方法,也可以调用在抽象类的子类中实现的基本方法,还可以调用其他对象中的方法。 |
具体子类 (ConcreteClass) |
它是抽象类的子类,用于实现在父类中声明的抽象基本操作以完成子类特定算法的步骤,也可以覆盖在父类中已实现的具体基本操作。 |
场景分析
特性 | 描述 |
---|---|
优点 | 在父类中形式化地定义一个算法,而由它的子类来实现细节的处理,在子类实现详细的处理算法时并不会改变算法中步骤的执行次序。 |
模板方法模式是一种代码复用技术,在类库设计中尤为重要,它提取了类库中的公共行为,将公共行为放在父类中,而通过其子类实现不同的行为,它鼓励用户恰当地使用继承来实现代码复用。 |
|
模板方法模式可以实现一种反向控制结构,通过子类覆盖父类的钩子方法来决定某一特定步骤是否需要执行。 |
|
在模板方法模式中可以通过子类来覆盖父类的基本方法,不同的子类可以提供基本方法的不同实现,更换和增加新的子类很方便,符合单一职责原则和开闭原则。 |
|
缺点 | 在模板方法模式中需要为每一个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统更加庞大,设计也更加抽象,此时可结合桥接模式进行设计。 |
适用环境 | 对一些复杂的算法进行分割,将其算法中固定不变的部分设计为模板方法和父类具体方法,而一些可以改变的细节由其子类来实现。即一次性实现一个算法的不变部分,并将可变的行为留给子类来实现。 |
各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。 | |
需要通过子类来决定父类算法中的某个步骤是否执行,实现子类对父类的方向控制。 |
由于访问者模式的使用条件较为苛刻,本身结构也较为复杂,因此在实际应用中使用频率不是也别高。当系统中存在一个较为复杂的对象结构,且不同访问者对其所采取的操作也不相同时,可以考虑使用访问者模式进行设计。在 XML 文档解析、编译器的设计、复杂集合对象的处理等领域中访问者模式得到了一定的应用。
组成对象
结构 | 描述 |
---|---|
抽象访问者 (Visitor) |
抽象访问者为对象结构中的每一个具体元素类声明一个访问操作,从这个操作的名称或参数类型可以清楚地知道需要访问的具体元素的类型,具体访问者需要实现这些操作方法,定义对这些元素的访问操作。 |
具体访问者 (ConcreteVisitor) |
具体访问者实现了每个由抽象访问者声明的操作,每一个操作用于访问对象结构中一种类型的元素。 |
抽象元素 (Element) |
抽象元素一般是抽象类或者接口,它声明了一个 accept()方法,用于接受访问者的访问操作,该方法通常以一个抽象访问者作为参数。 |
具体元素 (ConcreteElement) |
具体元素实现了 accept() 方法,在 accept() 方法中调用访问者的访问方法以便完成对一个元素的操作。 |
对象结构 (ObjectStructure) |
对象结构是一个元素的集合,它用于存放元素对象,并且提供了遍历其内部原生的方法。对象结构可以结合组合模式来实现,也可以是一个简单的集合对象。 |
场景分析
特性 | 描述 |
---|---|
优点 | 在访问者模式中增加新的访问操作很方便。使用访问者模式,增加新的访问操作就意味着增加一个新的具体访问者类,实现简单,无须修改源代码,符合开闭原则。 |
访问者模式将有关元素对象的访问行为集中到一个访问者对象中,而不是分散在一个个的元素类中。类的职责更加清晰,有利于对象结构中元素对象的复用,相同的对象结构可以供多个不同的访问者访问。 |
|
访问者模式让用户能够在不修改现有元素类层次结构的情况下定义作用于该层次结构的操作。 |
|
缺点 | 在访问者模式中增加新的元素类很困难。在访问者模式中,每增加一个新的元素类都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作,这违背了开闭原则的要求。 |
访问者面膜是破坏了对象的封装性。访问者模式要求访问者对象访问并调用每一个元素对象的操作,这意味着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法供访问者访问。 |
|
适用环境 | 一个对象结构包含多个类型的对象,希望对这些对象实施一些依赖其具体类型的操作。在访问者中针对每一种具体的类型都提供了一个访问操作,不同类型的对象可以有不同的访问操作。 |
需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。访问者模式使得用户可以将相关的访问操作集中起来定义在访问者类中,对象结构可以被多个不同的访问者类所使用的,将对象本身与对象的访问操作分离。 |
|
对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。 |