设计模式概述
如何提高软件系统的可维护性和可复用性是面向对象程序设计思想需要解决的核心问题。在面向对象程序设计中,可维护性的复用是以设计原则为基础的。每一个设计原则都蕴含着面向对象程序设计的思想,可以从不同的角度提升一个软件系统的架构水平。
面向对象程序设计原则是为支撑可维护性的复用而诞生的,它们是从很多的设计方案中总结出来的指导性原则,通常体现在设计模式中。
面向对象程序设计原则
最常见的面向对象程序设计原则有如下七条:
- 单一职责原则,规定一个类只负责一个功能
- 开闭原则,规定软件实体应该对扩展开放,而对修改关闭
- 里氏替换原则,规定所有引用基类对象的地方应该能够透明的使用其子类对象
- 依赖倒置原则,规定抽象不应该依赖于细节,而细节应该依赖于抽象
- 接口隔离原则,规定使用多个专门的接口而不是使用单一的总接口
- 合成复用原则,规定应尽量使用对象组合而不是继承来达到复用的目的
- 迪米特法则,规定一个软件实体应该尽可能少地与其他实体发生相互作用
单一职责原则
单一职责原则
的定义是:应该有且只有一个引起类变化的原因。
遵循单一职责原则
会带来一定的收益:
- 类的复杂度降低
- 类的内聚性提升,并在一定程度上提升了系统的可维护性
- 变更带来的风险降低
单一职责原则
是实现高内聚、低耦合的指导性方针。单一职责原则
从职责的层面将不同的职责封装在不同的类中,以此来控制类的粒度,以达到高耦合、低内聚的设计目标。
单一职责原则
要求开发人员从职责的角度发现类的不同职责并将其分离,从另一个角度来说,由于类的职责是一个无法量化的概念,在一定程度上需要依赖开发人员较强的分析能力和设计经验,可能在一定程度上会增加系统的复杂性。
单一职责原则
的目的是约束类的职责,在一定范围内尽可能保证有且只有一个引起类变化的原因。从代码设计的角度来说,单一职责原则
不仅适用于类的设计,也适用于方法的设计。在大的职责尺度上,同样也适用于软件系统中模块的设计。
单一职责原则
需要在大量的业务迭代实践中进行探索。从代码设计的角度来说,很难在第一时间设计出符合单一职责原则
的类(当然,方法的设计和实现还是相对容易符合该原则的),这需要前期对需求的极致细化和后期根据业需求进行重构。
Java
语言中 interface
的概念可以在一定意义上帮助我们设计出符合单一职责原则
的类。每一个不同的职责都对应一个广泛意义上的interface
,多个细粒度的职责(也就是多个 interface
)组合成了更大粒度上的新的职责。
由于职责
概念的模糊化,我们很难说 Android
源代码中某一个类的设计是完全符合单一职责原则
的。Activity
就只负责所有与用户交互的业务,Application
只负责应用程序全局的业务,从一定程度上来说,是符合单一职责原则
的。
开闭原则
开闭原则
是面向对象的可复用设计的第一块基石,是 Java
语言里面最为重要的设计原则。开闭原则
规定,一个软件实体(如类、模块和方法)应该对扩展开放,而对修改关闭。也就是说,一个软件实体应该通过扩展来实现变化而不是通过修改已有的代码。
Java
语言的 抽象
特性是开闭原则
的关键。在实际的开发工程中,可以为应用程序架构定义一个相对稳定的抽象层,而将不同的实现迁移到具体的实现层中完成。Java
语言提供了 抽象类
和 interface
等强大的抽象工具用来实现上述目的。
从一定的角度来说,Android
系统本身就是一种符合开闭原则
的软件实体。相较于功能机,Android
系统允许我们通过安装应用程序来扩展系统功能,而 Android Framework
层就是上述所提到的抽象层。更进一步的说,每一个应用程序可以通过添加 Activity
、Service
和 BroadcastReceiver
等组件实现不同的需求,而不是通过修改系统中已经存在的 Activity
、Service
和 BroadcastReceiver
等组件。
开闭原则
在应用程序设计方面的体现是,使用抽象(抽象类和 interface
)构建应用程序架构,使用具体实现扩展应用程序细节。
从 Android
应用程序架构的角度来说,业务需求在架构上的体现应该是稳定的,每个模块架构在代码上的描述是基于抽象类或 interface
的。这种基于抽象约束的方法主要体现在如下三个方面:
- 通过抽象类和
interface
约束扩展,对扩展进行边界的限定,不允许出现在抽象类和interface
中不存在的公共方法; - 参数类型和饮用对象尽量使用抽象类或
interface
,而不是使用实现类 - 保持抽象层的业务稳定
从 Android
应用程序具体实现的角度来说,对变化进行封装也是开闭原则
的具体体现。这种封装变化的方法主要体现在如下两个方面:
- 将相同的变化抽象到同一个抽象类或
interface
中 - 将不同的变化封装到不同的抽象类和
interface
中,不应该有两个不同的变化出现在同一个抽象类和interface
中
从项目管理的角度来说,建立项目开发规范,指定所有项目参与人员都必须遵守的约定也是较为不错的实践。就项目来说,约定由于配置。Android
系统中,基于开闭原则
设计的类随处可见。拿最为常见的集合对象来说,List
通过其接口定义了对外提供服务的接口,并额外提供了 ArrayList
和 LinkedList
的具体实现。在实际的使用过程中,我们可以根据需要选择具体的实现,也可以通过派生 List
的方式自己实现满足我们需求的子类。这个过程中,我们是通过 List
派生新的子类以满足我们的需求的,而不是修改已有的子类实现(例如给 ArrayList
增加线程安全机制)。
在 Android
的 View
体系中,Button
和 CheckBox
等控件都是派生自 TextView
。如果想修改 TextView
显示文本的样式,只要选择合适的 TextView
子类实现或者重写 TextView
样式相关的方法即可。View
体系在渲染时并不关心你所使用的 TextView
子类,它只是关心你的实现是否符合 TextView
的约束。这也就意味着,Android
的 View
体系也是符合开闭原则
的。
里氏替换原则
继承
是面向对象程序设计语言的重要特性,合理的使用继承
可以带来如下收益:
- 代码共享,降低创建类的工作量
- 提高代码和业务的可复用性
- 提高代码的可扩展性
但是由于继承
是一种侵入性的特性,只要是通过继承得到的子类,就必须拥有父类所有的方法和属性。这在一定程度上降低了代码的灵活性并增强了类之间(子类和父类)的耦合性。设想一下,如果由于某些原因,父类对其中的方法或属性进行了修改,这对其派生出来的子类来说,可能会非常糟糕——大量的代码需要调整。
里氏替换原则
为继续定义了一个规范。里氏替换原则
规定,所有使用父类的地方必须能够透明的使用其子类对象:
- 子类必须完全实现父类的方法
- 子类可以拥有自己的属性和方法
- 覆盖或实现父类方法时,输入的参数(参数列表)可以被放大
- 覆盖或实现父类的方法时,输出的结果(返回值)可以被缩小
- 覆盖或实现父类的方法时,其访问控制权限可以被方法
里氏替换原则
是实现开闭原则
的重要方法之一。由于使用父类的地方都可以使用子类对象,因此在代码设计中应尽量使用父类型来定义对象,而在应用程序运行时再确定其子类型。这也是 Java
语言中多态
特性的重要体现。
里氏替换原则
是我们在使用 Java
语言的继承
特性时最为重要的行为准则。幸运的是,在代码的编译阶段,Java
编译器会帮助我们检查一个子类是否符合里氏替换原则
。
在 Android
应用程序开发过程中,我们无时无刻不在使用里氏替换原则
带来的便利:
- 在实现
Activity
时,我们可以通过extends
关键字从Activity
派生我们自己的MyActivity
,重写必要的方法并未MyActivity
添加额外的方法,Android
系统依旧可以正确加载Activity
- 所有使用
Context
参数的地方,我们都可以使用MyActivity
进行替代 - 使用集合
List
的对象时,我们可以选择实例化ArrayList
也可以选择LinkedList
,这对具体的业务来说并没有任何影响(单纯从接口的角度来说,不考虑线程安全的问题)
Android
源代码中这样的示例数不胜数。在实际的开发过程中,我们应该尽可能的避免子类拥有额外的职责
(也就是向子类中添加额外的公共方法,违反里氏替换原则
),一方面这会破坏类设计的单一职责原则
,另一方面会由于子类添加的额外的公共方法而使得我们不得不使用子类对象取代原来的父类对象对外界提供服务,一定程度上提升了维护的难度。
里氏替换原则
的深层次目的则是要求我们在代码设计时,应尽可能的使用父类对象对外提供服务或与外界产生耦合。这与依赖倒置原则
有着相同的目的。
依赖倒置原则
如果说开闭原则
是面向对象程序设计的目标的话,那么依赖倒置原则
就是面向对象程序设计的主要实现机制。依赖倒置原则
规定,抽象不应该依赖于细节,而细节应该依赖于抽象。该原则指定了一种特定的的解耦形式,使得高层级的模块不依赖于低层级的模块的具体实现细节。依赖倒置原则
有如下几个关键点:
- 高层级的模块不应该依赖低层级的模块,两者都应该依赖于其自身的抽象
- 抽象不应该依赖于细节
- 细节应该依赖于抽象
依赖倒置原则
在 Java
语言中的具体表现就是:模块间的依赖通过抽象产生,子类之间不产生直接的依赖关系,其依赖关系是通过抽象类和 interface
产生的。依赖倒置原则
的核心思想是面向抽象编程而不是面向具体实现编程。这也就要求我们在代码设计时,应该尽量使用高层级的抽象。也就是说,使用抽象类和 interface
进行变量类型、参数类型和返回值类型的声明,而不是使用具体的实现。
在实现依赖倒置原则
时,我们需要针对抽象层进行编程,而将具体的实现细节的对象通过依赖注入
的方式注入到其他对象中。
依赖注入
是指当一个对象要与其他对象产生依赖关系时,通过抽象来注入所依赖的对象。常用的注入方式有三种:
- 构造注入:通过构造函数来传入具体子类的对象
- 设值注入(
setter 注入
):通过setter
方法传入具体子类的对象- 接口注入:在j
依赖倒置原则
解决的是类与类之间的耦合关系。如果类与类之间的通信直接依赖于具体的细节,那么它们之间就有着强耦合关系。当具体的实现细节出现变化时,也就意味着需要同时修改这些依赖具体细节的类。这在一定程度上降低了代码的可维护性。
依赖倒置原则
的本质就是通过抽象使得各个类之间彼此独立,不互相影响。从代码的角度来说,实现依赖倒置原则
应该符合如下规范:
- 每个类都尽量有其抽象类或
interface
- 变量的声明类型尽量使用抽象
- 任何类应该都是派生于抽象类
- 子类尽量不要覆盖父类的方法
- 结合
里氏替换原则
使用
接口隔离原则
Java
语言中,接口有两种表现形式:
- 实例接口:通过
new
创建的类的实例,它是对一种事物类型的描述 - 类接口:使用
interface
关键字声明的类
接口隔离原则
规定类之间的依赖关系应该建立在最小的接口上。也就是说,当一个接口太大时,我们需要将其分割为更小的接口,使用该接口的对象只需要知道与之相关的方法即可。这也就要求我们在设计接口时,应该尽可能的细化接口,并且保证接口中的方法尽量的少。
接口隔离原则
与单一职责原则
在概念上较为详尽,但是其本质的区别在于,前者规范的是接口内方法的数量,而后者则是强调接口的职责单一。
与里氏替换原则
规范的是继承
的行为一样,接口隔离原则
是对接口进行规范:
- 接口应该尽可能的小
- 接口要高内聚
- 只提供访问者需要的方法
与单一职责原则
中职责
的概念无法量化一样,接口的粒度大小问题也是一个无法量化的概念。接口的粒度越小,代码越灵活,但也在一定程度上增加了代码的复杂性,增加了维护的成本。
在软件开发过程中,确保一个接口只服务于一个模块,同时根据业务需求不断地压缩接口中的公共方法,细化为更细粒度的接口,是接口隔离原则
的不错的实践。
合成复用原则
合成复用原则
规定,我们在产生新的类的时候,应尽可能使用组合而不是继承的方式。在面向对象程序设计中,可以通过如下两种方式在不同的环境中复用已有的的设计和实现:
- 通过组合(聚合)的方式:该方式强调使用已有对象的实例组合成新的对象,已有对象与新对象之间不存在继承关系
- 通过继承的方式:该方式强调通过使用继承的方式产生新的对象,已有对象与新对象之间存在继承关系
使用组合的方式产生新的对象,可以将已经存在的对象包含到新的对象中,使之成为新对象中的一部分。这种方式可以保证新的对象可以使用已有对象的功能而无需暴露已有对象的实现细节。相对于继承的方式来说,这种方式耦合性相对较低,已有对象的变化对新对象的影响不会很大。
迪米特法则
迪米特法则
也称之为最少知识原则
。该原则规定,一个对象应该对其他对象有最少的了解。也就是说,一个类应该对自己需要耦合或调用的类知道的越少越好。
迪米特法则
的核心是解决类与类之间的耦合问题,并以此来限制类与类之间通信的宽度和深度。如果一个软件系统符合迪米特法则
,那么当其中的某一个模块发生改变时,对其他模块的影响将会变得非常小。该原则要求我们,应该尽量避免对象之间的交互。如果两个对象之间不必彼此直接通信,那么这两个对象就不应该发生任何的直接的相互作用。而如果两个对象之间必须产生某些调用行为的话,可以通过第三者对象转发这个调用。
上述方式体在代码开发过程中的体现就是:
- 在类的划分上,应该尽可能创建松耦合的二类
- 在类的设计上,每一个类都应该尽可能降低其他成员变量好成员方法的访问权限,如果可能的话,一个类应该设计为不变类(使用
final
修饰) - 在对其他类的引用上,一个对象对其他对象的引用应当降到最低
创建型设计模式
创建型设计模式对类的实例化过程进行了抽象,能够将软件系统中对象的创建和对象的使用过程分离。为了使软件系统的结构更加清晰,调用者对于这些对象只需要关心它们共同的接口,而不关心其具体的实现细节,使整个系统的设计更加符合单一职责原则。
创建型设计模式在创建什么(What
),由谁创建(Who
),何时创建(When
)等方面都为软件设计者提供了尽可能大的灵活性。创建型设计模式隐藏了类实例的创建细节,通过隐藏对象如何被创建的方式达到使得整个系统相对独立的目的。
创建型设计模式包含如下六种设计模式:
- 简单工厂模式:定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类
- 工厂方法模式:定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行
- 抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类
- 单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点
- 建造者模式:杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示
- 原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象
简单工厂模式
意图: 定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类
解决的问题: 解决产品对象选择的问题
使用时机: 有明确的计划在不同条件下创建不同的产品对象实例时
角色: 在简单工厂模式
中包含如下几个角色:
- 工厂角色(
Factory
):工厂角色也就是工厂类,它是简单工厂模式
的核心,负责创建所有的产品实例的内部逻辑。工厂类可以被客户端直接调用用来创建所需的产品实例 - 抽象产品角色(
Product
):抽象产品角色是工厂角色所创建的所有产品的抽象父类,它封装了各个产品实例的公共方法 - 具体产品角色(
ConcreteProduct
):具体产品角色是简单工厂模式
的创建目标。每一个具体产品角色都是由抽象产品角色派生的,所有被创建的产品实例都属于该角色
简单工厂模式
(Simple Factory Pattern
):又称为静态工厂方法
(Static Factory Method
)模式,它属于类创建型模式。在简单工厂模式
中,可以根据输入参数的不同返回不同产品角色的实例。简单工厂模式
专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。简单工厂模式
中各个角色的代码实现如下:
工厂角色(Factory
)的代码实现:
1 | public class Factory { |
抽象产品角色(Product
)的代码的代码实现:
1 | public interface Product { |
具体产品角色(ConcreteProduct
)的代码实现:
1 | public class ConcreteProductA implements Product { |
1 | public class ConcreteProductB implements Product { |
调用者的代码实现:
1 | Product product = Factory.createProduct("ProductA"); |
简单工厂模式
的主要优点如下:
- 工厂角色包含必要的判断逻辑,可以决定在什么条件下创建哪一个产品对象的实例,客户端可以免除直接创建产品对象的职责,而仅仅使用产品对象,实现了对象创建和使用的分离
- 客户端无须知道所创建的具体产品对象的类名,只需要知道具体产品对象所对应的参数即可,对于一些复杂的产品名称,通过
简单工厂模式
可以在一定程度减少使用者的记忆量 - 通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品角色,在一定程度上提高了系统的灵活性。
简单工厂模式
的主要缺点如下:
- 由于工厂角色集中了所有产品的创建逻辑,职责过重,违背
单一职责原则
的规范 - 使用
简单工厂模式
会增加系统中类的个数(引入了新的工厂角色),增加了系统的复杂度和理解难度 - 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护
简单工厂模式
由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构。
简单工厂模式
的使用场景:
- 工厂角色负责创建的对象比较少,不会造成工厂方法中的业务逻辑太过复杂
- 客户端只需要知道传入工厂角色的参数,对于如何创建对象并不关心
工厂方法模式
意图: 定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行
解决的问题: 解决产品对象选择的问题
使用时机: 有明确的计划在不同条件下创建不同的产品对象实例时
角色: 在工厂方法模式
中包含如下几个角色:
- 抽象工厂角色(
Factory
):在抽象工厂中,声明了工厂方法用于返回产品对象 - 具体工厂角色(
ConcreteFactory
):它是抽象工厂的子类,实现了抽象工厂中定义的的工厂方法,由客户端调用之后返回具体的产品对象 - 抽象产品角色(
Product
):它是定义产品角色的接口,是工厂方法中所创建的产品的父类 - 具体产品角色(
ConcreteProduct
):它实现了抽象产品的接口,是产品对象的具体实现。每一种类型的产品都由专门的具体工厂负责创建
工厂方法模式
(Factory Method Pattern
)又称为工厂模式
,它属于类创建型模式。在工厂方法模式
中,抽象工厂角色负责定义创建产品对象的公共接口,而具体工厂角色则负责生产具体的产品角色对象,这样做的目的是将产品对象的实例化过程延迟到具体工厂角色中完成,即通过具体工厂角色来确定究竟应该实例化哪一个具体产品角色。工厂方法模式
中各个角色的代码实现如下:
抽象产品角色(Product
)的代码实现:
1 | public interface Product { |
具体产品角色(ConcreteProduct
)的代码实现:
1 | public class ConcreteProductA implements Product { |
1 | public class ConcreteProductB implements Product { |
抽象工厂角色(Factory
)的代码实现:
1 | public abstract class Factory { |
具体工厂角色(ConcreteFactory
)的代码实现:
1 | public class ConcreteFactoryA extends Factory { |
1 | public class ConcreteFactoryB extends Factory { |
调用者的代码实现:
1 | Factory factoryA = new ConcreteFactoryA(); |
当软件系统中引入新的产品时,简单工厂模式
由于需要通过所传入的参数来创建不同的产品对象,这会要求我们对已有的工厂角色进行修改,在一定程度上违背了开闭原则
的规范。而且,由于简单工厂模式
中只存在一个工厂角色,该工厂角色位于产品对象创建的最核心位置,它需要知道每一个产品对象的创建细节,并根据条件决定创建哪一个产品对象,在一定程度上造成了工厂角色的职责过于繁重,而且产品对象与工厂角色之间高度耦合,影响了软件系统的灵活性和可扩展性。
工厂方法模式
弥补了简单工厂模式
在上述方面的不足。与简单工厂模式
不同的是,在工厂方法模式
中不再提供一个统一的工厂角色用于创建所有的产品对象,取而代之的是针对不同的产品提供不同的工厂角色。
工厂方法模式
的主要优点如下:
- 在
工厂方法模式
中,工厂方法用来创建客户端所需要的产品对象,同时还向客户端隐藏了哪种具体产品角色将被实例化这一细节,客户端只需要关心所需产品对象对应的工厂,而无须关心创建细节,甚至无须知道具体产品对象的类名; - 基于工厂角色和产品角色的多态性设计是
工厂方法模式的关键
。它能够让工厂角色可以自主确定创建何种产品对象,且创建对象的细节完全封装在具体工厂内部; - 使用
工厂方法模式
的另一个优点是在系统中加入新产品角色时,无须修改抽象工厂角色和抽象产品角色提供的接口,无须修改客户端,也无须修改其他的具体工厂角色和具体产品角色,而只要添加一个具体工厂角色和具体产品角色就可以了。这样,系统的可扩展性也就变得非常好,完全符合“开闭原则”。
工厂方法模式
的主要缺点如下:
- 在添加新产品时,需要编写新的具体产品角色类,而且还要提供与之对应的具体工厂角色类,造成系统中类的个数将成对的增加,在一定程度上增加了系统的复杂度;
- 由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度。
工厂方法模式
的使用场景如下:
- 客户端不需要知道它所需要的对象的类。在
工厂方法模式
中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建; - 抽象工厂角色通过其子类(具体工厂角色)来指定创建哪个产品对象。在
工厂方法模式
中,对于抽象工厂角色只需要提供一个创建产品对象的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则
,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。
抽象工厂模式
意图: 提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类
解决的问题: 主要解决接口选择的问题
使用时机: 系统的产品有多于一个的产品族,而系统只消费其中某一族的产品
角色: 在抽象工厂模式
中包含如下几个角色:
- 抽象工厂(
Abstract Factory
):它声明了一组用于创建一族的产品的方法,每一个方法对应一种产品 - 具体工厂(
Concrete Factory
):它实现了在抽象工厂中声明的创建产品的方法,生成一组具体产品,这些产品构成了一个产品族,每一个产品都位于某个产品等级结构中 - 具体产品(
Concrete Product
):它定义了具体工厂生成的具体产品对象,实现了抽象产品接口中的公共方法 - 抽象产品(
Abstract Product
):它为每种产品声明接口,在抽象产品中声明了产品所具有的公共方法
抽象工厂模式
(Abstract Factory Pattern
)提供了一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。在抽象工厂模式
中,每一个具体工厂都提供了多个工厂方法用于创建多种不同类型的产品。这些产品构成了抽象产品族
。抽象工厂模式
中各个角色的代码实现如下:
抽象产品族
是由同一个工厂角色
生产的,位于不同产品等级结构
中的一组产品。产品等级结构
指的是产品的继承结构。
抽象工厂(Abstract Factory
)的代码实现:
1 | public abstract class AbstractFactory { |
具体工厂(Concrete Factory
)的代码实现:
1 | public class ConcreteFactory extends AbstractFactory { |
抽象产品(Abstract Product
)的代码实现:
1 | public interface AbstractProductA { |
具体产品(Concrete Product
)的代码实现:
1 | public class ConcreteProductB implements AbstractProductB { |
抽象工厂模式
的主要优点如下:
抽象工厂模式
隔离了具体类的生成,使得客户端并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易,所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为- 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象
- 增加新的产品族很方便,无须修改已有系统,符合
开闭原则
的规范
抽象工厂模式
的主要缺点如下:
增加新的产品等级结构比较麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,违背了开闭原则
的规范。
抽象工厂模式
的使用场景:
- 一个系统不依赖于产品类实例如何被创建、组合和表达的细节,用户无须关心对象的创建过程,将对象的创建和使用解耦
- 系统中有多于一个的产品族,而每次只使用其中某一产品族。可以通过配置文件等方式来使得用户可以动态改变产品族,也可以很方便地增加新的产品族
- 属于同一个产品族的产品在一起使用,这一约束必须在系统的设计中体现出来。同一个产品族中的产品可以是没有任何关系的对象,但是它们都具有一些共同的约束,如同一操作系统下的按钮和文本框,按钮与文本框之间没有直接关系,但它们都是属于某一操作系统的,此时具有一个共同的约束条件:操作系统的类型
- 产品等级结构稳定,设计完成之后,不会向系统中增加新的产品等级结构或者删除已有的产品等级结构
单例模式
意图: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
解决的问题: 一个全局使用的类频繁地创建与销毁。
使用时机: 当您想控制实例数目,节省系统资源的时候。
角色: 在单例模式
中包含如下角色:
- 单例对象(
Singleton
):单例模式
中唯一的角色,负责单例对象的创建,并提供全局的接口对外提供该对象
单例模式
(Singleton Pattern
)确保系统中某一个类有且只有一个实例,且能够自行实例化并向整个系统提供这个实例。单例模式
中各个角色的代码实现如下:
单例模式
有多种实现方式。如果只是在单线程环境下使用,那么可以考虑使用如下实现方式:
1 | public class Singleton { |
上述方式是单例模式
最基本的实现。由于没有考虑线程同步的问题,所以只能在单线程环境下使用。单例类可以在 getInstance()
被调用时得到初始化。如果需要在多线程环境下使用单例模式
,就需要考虑线程同步的问题,而且由于多线程环境下对象初始化的时机问题,我们使用双重校验锁
的方式实现单例模式
:
1 | public class Singleton { |
上述方式在保证多线程下对象同步的同时,通过使用双重校验锁
的机制保证了多线程环境下对象初始化时机的问题。使用静态内部类的方式也可以达到同样的目的,而且实现难度和效率更为高效:
1 | public class Singleton { |
单例模式
提供了一种对唯一对象实例的受控访问,可以严格控制调用者对该实例的访问。而对于那些资源开销开销较大的资源来说,确保系统中只有一个对象实例对外提供服务,可以在一定程度上节省系统资源。但是,在单例模式中,由于单例类既要充当工厂角色
提供工厂方法,又要充当产品角色
提供产品业务,在一定程度上违背了单一职责原则
。对于 Java
语言来说,由于垃圾回收机制
的存在,如果单例对象长时间不活跃,那么会存在被垃圾回收器
回收的风险。当再次使用对象时会被重新初始化,从而造成对象数据的丢失。
单例模式
的主要优点如下:
单例模式
提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户端怎样以及何时访问它- 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,
单例模式
无疑可以提高系统的性能 - 允许可变数目的实例。基于
单例模式
我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例对象共享过多有损性能的问题
单例模式
的主要缺点如下:
- 由于
单例模式
中没有抽象层,因此单例类的扩展有很大的困难 - 单例类的职责过重,在一定程度上违背了
单一职责原则
。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到了一起 Java
语言的运行时环境提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾对象,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失
单例模式
的使用场景:
- 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象
- 客户端调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例
原型模式
意图: 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象
解决的问题: 在运行期建立和删除原型
角色: 原型模式
中有如下几个角色:
- 抽象原型角色(
Protocol Type
):它是声明克隆方法的接口,是所有具体原型类的公共父类,可以是抽象类也可以是接口,甚至还可以是具体实现类 - 具体原型角色(
Concrete Protocol Type
):它实现了抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象 - 客户类(
Client
):让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象
原型模式
(Prototype Pattern
)用于创建重复的对象,同时又能保证性能,它提供了一种创建对象的最佳方式。原型模式
实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。原型模式
中各个角色的代码实现如下:
抽象原型角色(Protocol Type
)的代码实现:
1 | public abstract class ProtocolType implements Cloneable { |
具体原型角色(Concrete Protocol Type
)的代码实现:
1 | public class ConcreteProtocolType extends ProtocolType { |
调用者的代码实现:
1 | public class Client { |
在 java
语言中,数据类型分为值类型
(基本数据类型)和引用类型
。Java
中的对象克隆分为如下两种形式:
- 浅克隆:在
浅克隆
中,如果原型对象的成员变量是值类型
,将复制一份给克隆对象。如果原型对象的成员变量是引用类型
,则将引用对象的地址复制一份给克隆对象(即:原型对象和克隆对象的成员变量指向相同的内存地址)。换句话说,在浅克隆
中,当对象被复制时只复制它本身和其中包含的值类型
的成员变量,而引用类型的成员变量并没有复制; - 深克隆:在
深克隆
中,无论原型对象的成员变量是值类型
还是引用类型
,都将复制一份给克隆对象。深克隆
将原型对象的所有引用对象也复制一份给克隆对象(即:在深克隆
中,对了对象本身被克隆意外,对象所包含的所有成员变量也会被克隆)。
原型模式
的主要优点如下:
- 当需要创建的新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率
- 扩展性较好,由于在
原型模式
中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类写在配置文件中,增加或减少产品类对原有系统都没有任何影响 原型模式
提供了简化的创建结构,工厂方法模式
常常需要有一个与产品类等级结构相同的工厂等级结构,而原型模式
就不需要这样,原型模式
中产品的克隆是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品- 可以使用深克隆的方式保存对象的状态,使用原型模式将对象克隆一份并将其状态保存起来,以便在需要的时候使用(如恢复到某一历史状态),可辅助实现撤销操作
原型模式
的主要缺点如下:
- 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时,需要修改源代码,违背了
开闭原则
- 在实现
深克隆
时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆
,每一层对象对应的类都必须支持深克隆
,实现起来可能会比较麻烦
原型模式
的使用场景
- 创建新对象成本较大(如初始化需要占用较长的时间,占用太多的
CPU
资源或网络资源),新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其成员变量稍作修改 - 如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占用内存较少时,可以使用
原型模式
配合备忘录模式
来实现 - 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新的实例可能比使用构造函数创建一个新实例更加方便
建造者模式
意图: 将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示
解决的问题: 主要解决在软件系统中,有时候面临着一个复杂对象
的创建工作,其通常由各个部分的子对象用一定的业务构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的业务却相对稳定
角色: 建造者模式
中有如下角色:
- 抽象建造者(
Builder
):他为创建一个产品角色的各个组成部分指定抽象接口,在该接口中一般声明两类方法:- 用于构建各个组成部分的方法,一般使用
buildXXX()
- 用于返回构建结果的方法,一般使用
getProduct()
- 用于构建各个组成部分的方法,一般使用
- 产品角色(
Product
):它是被构建的复杂对象,包含多个组成部分,具体建造者创建该产品的内部表示并定义它的装配过程 - 具体建造者(
ConcreteBuilder
):它实现了Builder
的接口,实现各个组成部分的具体构造和装配方法,定义并明确它所创建的复杂对象,也可以提供一个方法用于返回创建好的复杂对象 - 指挥者(
Director
):指挥者又称导演类,它负责安排复杂对象的建造顺序,指挥者与抽象建造者之间存在关联关系,可以在其产品构造方法(一般使用getProduct()
表示)调用具体建造者的部件构造和装配方法,完成复杂对象的建造
建造者模式
(Builder Pattern
)又称为生成器模式
,其定义是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建出不同的表示。其目的是通过使用多个简单的对象来构建一个复杂的对象。它将客户端与包含多个组成部分的复杂对象的创建过程分离,客户端无需知道复杂对象的内部组成部分与装配方式,只需要知道所需建造者的类型即可。建造者模式
中各个角色的代码实现如下:
抽象建造者(Builder
)的代码实现:
1 | public abstract class Builder { |
具体建造者(ConcreteBuilder
)的代码实现:
1 | public class ConcreteBuilder extends Builder { |
产品角色(Product
)的代码实现:
1 | public class Product { |
指挥者(Director
)的代码实现:
1 | public class Director { |
调用者代码实现:
1 | Builder builder = new ConcreteBuilder(); |
建造者模式
的主要优点如下:
- 在
建造者模式
中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象 - 每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象。由于指挥者类针对抽象建造者编程,增加新的具体建造者无须修改原有类库的代码,系统扩展方便,符合
开闭原则
- 可以更加精细地控制产品的创建过程。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程
建造者模式
的主要缺点如下:
建造者模式
所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,例如很多组成部分都不相同,不适合使用建造者模式
,因此其使用范围受到一定的限制- 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大,增加系统的理解难度和运行成本
建造者模式
使用场景:
- 需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性
- 需要生成的产品对象的属性相互依赖,需要指定其生成顺序
- 对象的创建过程独立于创建该对象的类。在
建造者模式中
通过引入了指挥者类,将创建过程封装在指挥者类中,而不在建造者类和客户类中 - 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品
结构型设计模式
结构型设计模式(Structural Pattern
)描述如何将类或者对象结合在一起形成更大的结构。结构型设计模式可以分为类结构型模式和对象结构型模式:
- 类结构型模式关心类的组合,由多个类可以组合成一个更大的系统,在类结构型模式中一般只存在继承关系和实现关系
- 对象结构型模式关心类与对象的组合,通过关联关系使得在一个类中定义另一个类的实例对象,然后通过该对象调用其方法。
根据合成复用原则
,在系统中尽量使用关联关系来替代继承关系,因此大部分结构型模式都是对象结构型模式。
结构型设计模式
包含如下几种设计模式:
- 适配器模式:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作
- 桥接模式:将抽象部分与实现部分分离,使它们都可以独立的变化
- 组合模式:将对象组合成树形结构以表示”部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性
- 装饰模式:动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活
- 外观模式:为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用
- 享元模式:运用共享技术有效地支持大量细粒度的对象
- 代理模式:为其他对象提供一种代理以控制对这个对象的访问
适配器模式
意图: 将一个类的接口转换成客户端希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作
解决的问题: 主要解决在软件系统中,常常要将一些现存的对象
放到新的环境中,而新环境要求的接口是现对象不能满足的
角色: 适配器模式
中包含如下几个角色:
- 目标抽象类(
Target
): 目标抽象类定义客户端所需接口,可以是一个抽象类或接口,也可以是具体类 - 适配器类(
Adapter
):适配器可以调用另一个接口,作为一个转换器,对Adaptee
和Target
进行适配,适配器类是适配器模式的核心,在对象适配器中,它通过继承Target
并关联一个Adaptee
对象使二者产生联系 - 适配者类(
Adaptee
):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码
适配器模式
(Adapter Pattern
) :将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper
)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。适配器模式
中各个角色的代码实现如下:
目标抽象类(Target
)的代码实现:
1 | public interface Target { |
适配者类(Adaptee
)的代码实现:
1 | public class Adaptee { |
适配器类(Adapter
)的代码实现:
1 | public class Adapter extends Adaptee implements Target { |
调用方式代码如下:
1 | public class Client { |
无论是对象适配器模式还是类适配器模式都具有如下优点:
- 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构
- 增加了类的透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用
- 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合
开闭原则
具体来说,类适配器模式还有如下优点:
- 由于适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强
对象适配器模式还有如下优点:
- 一个对象适配器可以把多个不同的适配者适配到同一个目标
- 可以适配一个适配者的子类,由于适配器和适配者之间是关联关系,根据
里氏代换原则
,适配者的子类也可通过该适配器进行适配
类适配器模式的缺点如下:
- 对于
Java
这种不支持多重类继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配者 - 适配者类不能为最终类,如在
Java
中不能为final
类 - 在
Java
语言中,类适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性
对象适配器模式的缺点如下:
- 与类适配器模式相比,要在适配器中置换适配者类的某些方法比较麻烦。如果一定要置换掉适配者类的一个或多个方法,可以先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂。
适配器模式
使用场景:
- 系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需要,甚至没有这些类的源代码
- 想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作
桥接模式
意图: 将抽象部分与实现部分分离,使它们都可以独立的变化
解决的问题: 在有多种可能会变化的情况下,用继承会造成类爆炸问题,扩展起来不灵活
角色: 桥接模式
中存在如下角色:
- 抽象类(
Abstraction
):用于定义抽象类的接口,它一般是抽象类而不是接口,其中定义了一个Implementor
(实现类接口)类型的对象并可以维护该对象,它与Implementor
之间具有关联关系,它既可以包含抽象业务方法,也可以包含具体业务方法 - 扩充抽象类(
RefinedAbstraction
):扩充由Abstraction
定义的接口,通常情况下它不再是抽象类而是具体类,它实现了在Abstraction
中声明的抽象业务方法,在RefinedAbstraction
中可以调用在Implementor
中定义的业务方法 - 实现类接口(
Implementor
):定义实现类的接口,这个接口不一定要与Abstraction
的接口完全一致,事实上这两个接口可以完全不同,一般而言,Implementor
接口仅提供基本操作,而Abstraction
定义的接口可能会做更多更复杂的操作。Implementor
接口对这些基本操作进行了声明,而具体实现交给其子类。通过关联关系,在Abstraction
中不仅拥有自己的方法,还可以调用到Implementor
中定义的方法,使用关联关系来替代继承关系 - 具体实现类(
ConcreteImplementor
):具体实现Implementor
接口,在不同的ConcreteImplementor
中提供基本操作的不同实现,在程序运行时,ConcreteImplementor
对象将替换其父类对象,提供给抽象类具体的业务操作方法
桥接模式
(Bridge Pattern
)将抽象部分与它的实现部分分离,使它们都可以独立地变化。桥接模式
中各个角色的代码实现如下:
抽象类(Abstraction
)的代码:
1 | public abstract class Abstraction { |
扩充抽象类(RefinedAbstraction
)的代码:
1 | public class RefinedAbstraction extends Abstraction { |
实现类接口(Implementor
)的代码:
1 | public interface Implementor { |
具体实现类(ConcreteImplementor
)的代码:
1 | public class ConcreteImplementor implements Implementor { |
桥接模式
的主要优点如下:
- 分离抽象接口及其实现部分。桥接模式使用
对象间的关联关系
解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。所谓抽象和实现沿着各自维度的变化,也就是说抽象和实现不再在同一个继承层次结构中,而是子类化
它们,使它们各自都具有自己的子类,以便任何组合子类,从而获得多维度组合对象 - 在很多情况下,
桥接模式
可以取代多层继承方案,多层继承方案违背了单一职责原则
,复用性较差,且类的个数非常多,桥接模式是比多层继承方案更好的解决方法,它极大减少了子类的个数 桥接模式
提高了系统的可扩展性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统,符合开闭原则
桥接模式
的主要缺点如下:
桥接模式
的使用会增加系统的理解与设计难度,由于关联关系建立在抽象层,要求开发者一开始就针对抽象层进行设计与编程桥接模式
要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性,如何正确识别两个独立维度也需要一定的经验积累
桥接模式
的使用场景:
- 如果一个系统需要在抽象化和具体化之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系,通过桥接模式可以使它们在抽象层建立一个关联关系
抽象部分
和实现部分
可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合- 一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立进行扩展
- 对于那些不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统,
桥接模式
尤为适用
组合模式
意图: 将对象组合成树形结构以表示部分-整体
的层次结构。组合模式
使得用户对单个对象和组合对象的使用具有一致性
解决的问题: 它在我们树型结构的问题中,模糊了简单元素和复杂元素的概念,客户端可以向处理简单元素一样来处理复杂元素,从而使得客户端与复杂元素的内部结构解耦
角色: 组合模式
有如下角色:
- 抽象构件(
Component
):它可以是接口或抽象类,为叶子构件和容器构件对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现。在抽象构件中定义了访问及管理它的子构件的方法,如增加子构件、删除子构件、获取子构件等 - 叶子构件(
Leaf
):它在组合结构中表示叶子节点对象,叶子节点没有子节点,它实现了在抽象构件中定义的行为。对于那些访问及管理子构件的方法,可以通过异常等方式进行处理 - 容器构件(
Composite
):它在组合结构中表示容器节点对象,容器节点包含子节点,其子节点可以是叶子节点,也可以是容器节点,它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括那些访问及管理子构件的方法,在其业务方法中可以递归调用其子节点的业务方法
组合模式
(Composite Pattern
)对单个对象(叶子构件)和组合对象(容器构件)具有一致性,它将对象组织到树结构中,可以用来描述整体与部分的关系。同时它也模糊了简单元素(叶子组件)和复杂元素(容器组件)的概念,使得客户端能够像处理简单元素一样来处理复杂元素,从而使客户程序能够与复杂元素的内部结构解耦。组合模式
中各个角色的关系代码实现如下:
抽象构件(Component
)的代码如下:
1 | public interface Employee { |
叶子构件(Leaf
)的代码如下:
1 | public class Developer implements Employee { |
容器构件(Composite
)的代码如下:
1 | public class Manager implements Employee { |
调用示例:
1 | public class Client { |
组合模式
的主要优点如下:
组合模式
可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制- 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码
- 在
组合模式中
增加新的容器构件和叶子构件都很方便,无须对现有类库进行任何修改,符合开闭原则
组合模式
为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子构件和容器构件的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单
组合模式
的主要缺点如下:
在增加新构件时很难对容器中的构件类型进行限制。有时候我们希望一个容器中只能有某些特定类型的对象,例如在某个文件夹中只能包含文本文件,使用组合模式时,不能依赖类型系统来施加这些约束,因为它们都来自于相同的抽象层,在这种情况下,必须通过在运行时进行类型检查来实现,这个实现过程较为复杂
组合模式
的使用场景:
- 在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致地对待它们
- 在一个使用面向对象语言开发的系统中需要处理一个树形结构
- 在一个系统中能够分离出叶子构件和容器构件,而且它们的类型不固定,需要增加一些新的类型
装饰模式
意图: 动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活
解决的问题: 一般的,我们为了扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀
角色: 装饰器模式
中有如下角色:
- 抽象构件(
Component
):它是具体构件和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法,它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作 - 具体构件(
ConcreteComponent
):它是抽象构件类的子类,用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责 - 抽象装饰类(
Decorator
):它也是抽象构件类的子类,用于给具体构件增加职责,但是具体职责在其子类中实现。它维护一个指向抽象构件对象的引用,通过该引用可以调用装饰之前构件对象的方法,并通过其子类扩展该方法,以达到装饰的目的 - 具体装饰类(
ConcreteDecorator
):它是抽象装饰类的子类,负责向构件添加新的职责。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为
装饰模式
(Decorator Pattern
)旨在动态地给一个对象增加一些额外的职责(Responsibility
),就增加对象功能来说,装饰模式
比生成子类实现更为灵活。其别名也可以称为包装器(Wrapper
),与适配器模式
的别名相同,但它们适用于不同的场合。装饰器模式
中各个角色的代码实现如下:
抽象构件(Component
)的代码:
1 | public interface Component { |
具体构件(ConcreteComponent
)的代码:
1 | public class ConcreteComponent implements Component { |
抽象装饰类(Decorator
)的代码:
1 | public class Decorated implements Component { |
具体装饰类(ConcreteDecorator
)的代码:
1 | public class ConcreteDecoratorA extends Decorated { |
调用示例:
1 | public class Client { |
装饰模式
的主要优点如下:
- 对于扩展一个对象的功能,
装饰模式
比继承更加灵活性,不会导致类的个数急剧增加 - 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的具体装饰类,从而实现不同的行为
- 可以对一个对象进行多次装饰,通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合,得到功能更为强大的对象
- 具体构件类与具体装饰类可以独立变化,客户端可以根据需要增加新的具体构件类和具体装饰类,原有类库代码无须改变,符合
开闭原则
装饰模式
的主要缺点如下:
- 使用
装饰模式
进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,大量小对象的产生势必会占用更多的系统资源,在一定程序上影响程序的性能 装饰模式
提供了一种比继承更加灵活机动的解决方案,但同时也意味着比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为繁琐
装饰模式
的使用场景如下:
- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责
- 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用
装饰模式
不能采用继承的情况主要有两类:
- 系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长
- 类已定义为不能被继承(如
Java
语言中的final
类)
外观模式
意图: 为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用
解决的问题: 降低访问复杂系统的内部子系统时的复杂度,简化客户端与之的接口
角色: 外观模式
有如下角色:
- 外观角色(
Facade
):在客户端可以调用它的方法,在外观角色
中可以知道相关的(一个或者多个)子系统的功能和责任;在正常情况下,它将所有从客户端发来的请求委派到相应的子系统去,传递给相应的子系统对象处理 - 子系统角色(
SubSystem
):在软件系统中可以有一个或者多个子系统角色,每一个子系统可以不是一个单独的类,而是一个类的集合,它实现子系统的功能;每一个子系统都可以被客户端直接调用,或者被外观角色调用,它处理由外观类传过来的请求;子系统并不知道外观的存在,对于子系统而言,外观角色仅仅是另外一个客户端而已
外观模式
(Facade Pattern
)是指外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。外观模式又称为门面模式
,它是一种对象结构型模式。外观模式
中各个角色的代码实现如下:
外观角色(Facade
)的代码:
1 | public class Facade { |
子系统角色(SubSystem
)的代码:
1 | public class SubSystemA { |
调用者的代码:
1 | public class Client { |
外观模式
的主要优点如下:
- 它对客户端屏蔽了子系统组件,减少了客户端所需处理的对象数目,并使得子系统使用起来更加容易。通过引入
外观模式
,客户端代码将变得很简单,与之关联的对象也很少 - 它实现了子系统与客户端之间的松耦合关系,这使得子系统的变化不会影响到调用它的客户端,只需要调整外观类即可
- 一个子系统的修改对其他子系统没有任何影响,而且子系统内部变化也不会影响到外观对象
外观模式
的主要缺点如下:
- 不能很好地限制客户端直接使用子系统类,如果对客户端访问子系统类做太多的限制则减少了可变性和灵活性
- 如果设计不当,增加新的子系统可能需要修改外观类的源代码,违背了
开闭原则
外观模式
的使用场景:
- 当要为访问一系列复杂的子系统提供一个简单入口时可以使用
外观模式
- 客户端程序与多个子系统之间存在很大的依赖性。引入外观类可以将子系统与客户端解耦,从而提高子系统的独立性和可移植性
- 在层次化结构中,可以使用
外观模式
定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度
享元模式
意图: 运用共享技术有效地支持大量细粒度的对象
解决的问题: 在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建
角色: 享元模式
有如下角色:
- 抽象享元类(
Flyweight
):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态) - 具体享元类(
ConcreteFlyweight
):它实现了抽象享元类,其实例称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象 - 非共享具体享元类(
UnsharedConcreteFlyweight
):并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建 - 享元工厂类(
FlyweightFactory
):享元工厂类用于创建并管理享元对象,它针对抽象享元类编程,将各种类型的具体享元对象存储在一个享元池中,享元池一般设计为一个存储键值对
的集合(也可以是其他类型的集合),可以结合工厂模式进行设计;当用户请求一个具体享元对象时,享元工厂提供一个存储在享元池中已创建的实例或者创建一个新的实例(如果不存在的话),返回新创建的实例并将其存储在享元池中
享元模式
(Flyweight Pattern
)通过运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。享元模式
中各个角色的代码实现如下:
抽象享元类(Flyweight
)的代码实现:
1 | public interface Flyweight { |
具体享元类(ConcreteFlyweight
)的代码实现:
1 | public class ConcreteFlyWeight implements Flyweight { |
享元工厂类(FlyweightFactory
)的代码实现:
1 | public class FlyweightFactory { |
享元模式
的主要优点如下:
- 可以极大减少内存中对象的数量,使得相同或相似对象在内存中只保存一份,从而可以节约系统资源,提高系统性能
享元模式
的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享
享元模式
的主要缺点如下:
享元模式
使得系统变得复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化- 为了使对象可以共享,
享元模式
需要将享元对象的部分状态外部化,而读取外部状态将使得运行时间变长
享元模式
的使用场景:
- 一个系统有大量相同或者相似的对象,造成内存的大量耗费
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中
- 在使用
享元模式
时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式
代理模式
意图: 为其他对象提供一种代理以控制对这个对象的访问
解决的问题: 在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层
角色: 代理模式
有如下角色:
- 抽象主题角色(
Subject
):它声明了真实主题和代理主题的共同接口,这样一来在任何使用真实主题的地方都可以使用代理主题,客户端通常需要针对抽象主题角色进行编程 - 代理主题角色(
Proxy
):它包含了对真实主题的引用,从而可以在任何时候操作真实主题对象;在代理主题角色中提供一个与真实主题角色相同的接口,以便在任何时候都可以替代真实主题;代理主题角色还可以控制对真实主题的使用,负责在需要的时候创建和删除真实主题对象,并对真实主题对象的使用加以约束。通常,在代理主题角色中,客户端在调用所引用的真实主题操作之前或之后还需要执行其他操作,而不仅仅是单纯调用真实主题对象中的操作 - 真实主题角色(
RealSubject
):它定义了代理角色所代表的真实对象,在真实主题角色中实现了真实的业务操作,客户端可以通过代理主题角色间接调用真实主题角色中定义的操作
代理模式
(Proxy Pattern
)给某一个对象提供一个代理,并由代理对象控制对原对象的引用。代理模式
中各个角色的代码实现如下:
抽象主题角色(Subject
)的代码实现:
1 | public interface Subject { |
代理主题角色(Proxy
)的代码实现:
1 | public class Proxy implements Subject { |
真实主题角色(RealSubject
)的代码实现:
1 | public class RealSubject implements Subject { |
调用者代码实现:
1 | public class Client { |
代理模式
的优点如下:
- 能够协调调用者和被调用者,在一定程度上降低了系统的耦合度
- 客户端可以针对抽象主题角色进行编程,增加和更换代理类无须修改源代码,符合
开闭原则
,系统具有较好的灵活性和可扩展性
此外,不同类型的代理模式也具有独特的优点,例如:
- 远程代理为位于两个不同地址空间对象的访问提供了一种实现机制,可以将一些消耗资源较多的对象和操作移至性能更好的计算机上,提高系统的整体运行效率
- 虚拟代理通过一个消耗资源较少的对象来代表一个消耗资源较多的对象,可以在一定程度上节省系统的运行开销
- 缓冲代理为某一个操作的结果提供临时的缓存存储空间,以便在后续使用中能够共享这些结果,优化系统性能,缩短执行时间
- 保护代理可以控制对一个对象的访问权限,为不同用户提供不同级别的使用权限
代理模式
的主要缺点如下:
- 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢,例如保护代理
- 实现
代理模式
需要额外的工作,而且有些代理模式
的实现过程较为复杂,例如远程代理
代理模式
的使用场景如下:
代理模式
的类型较多,不同类型的代理模式
有不同的优缺点,它们应用于不同的场合
- 当客户端对象需要访问远程主机中的对象时可以使用远程代理
- 当需要用一个消耗资源较少的对象来代表一个消耗资源较多的对象,从而降低系统开销、缩短运行时间时可以使用虚拟代理,例如一个对象需要很长时间才能完成加载时
- 当需要为某一个被频繁访问的操作结果提供一个临时存储空间,以供多个客户端共享访问这些结果时可以使用缓冲代理。通过使用缓冲代理,系统无须在客户端每一次访问时都重新执行操作,只需直接从临时缓冲区获取操作结果即可
- 当需要控制对一个对象的访问,为不同用户提供不同级别的访问权限时可以使用保护代理
- 当需要为一个对象的访问(引用)提供一些额外的操作时可以使用智能引用代理
行为型模式
行为型模式(Behavioral Pattern
)是对在不同的对象之间划分责任和算法的抽象化。行为型模式不仅仅关注类和对象的结构,而且重点关注它们之间的相互作用。
通过行为型模式,可以更加清晰地划分类与对象的职责,并研究系统在运行时实例对象之间的交互。在系统运行时,对象并不是孤立的,它们可以通过相互通信与协作完成某些复杂功能,一个对象在运行时也将影响到其他对象的运行。
行为型模式分为类行为型模式和对象行为型模式两种:
- 类行为型模式:类的行为型模式使用继承关系在几个类之间分配行为,类行为型模式主要通过多态等方式来分配父类与子类的职责
- 对象行为型模式:对象的行为型模式则使用对象的聚合关联关系来分配行为,对象行为型模式主要是通过对象关联等方式来分配两个或多个类的职责。根据
合成复用原则
,系统中要尽量使用关联关系来取代继承关系,因此大部分行为型设计模式都属于对象行为型设计模式
行为型模式包含如下设计模式:
- 责任链模式:避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止
- 命令模式:将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化
- 解释器模式:给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子
- 迭代器模式:提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部表示
- 中介者模式:一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互
- 备忘录模式:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态
- 观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新
- 状态模式:允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类
- 策略模式:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换
- 模板方法模式:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤
- 访问者模式:主要将数据结构与数据操作分离
责任链模式
意图: 避免请求的发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止
解决的问题: 职责连上的处理者负责处理请求,客户只需要将请求发送到职责链上即可,无须关心请求的处理细节和请求的传递,所以职责链将请求的发送者和请求的处理者解耦了
角色: 责任链模式
中有如下角色:
- 抽象处理者(
Handler
):它定义了一个处理请求的接口,一般设计为抽象类,由于不同的具体处理者处理请求的方式不同,因此在其中定义了抽象请求处理方法。因为每一个处理者的下家还是一个处理者,因此在抽象处理者中定义了一个抽象处理者类型的对象,作为其对下家的引用。通过该引用,处理者可以连成一条链 - 具体处理者(
ConcreteHandler
):它是抽象处理者的子类,可以处理用户请求,在具体处理者类中实现了抽象处理者中定义的抽象请求处理方法,在处理请求之前需要进行判断,看是否有相应的处理权限,如果可以处理请求就处理它,否则将请求转发给后继者;在具体处理者中可以访问链中下一个对象,以便请求的转发
责任链模式
(Chain of Responsibility Pattern
)避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。职责链模式是一种对象行为型模式。责任链模式
中各个角色的代码实现如下:
抽象处理者(Handler
)的代码实现:
1 | public abstract class Handler { |
具体处理者(ConcreteHandler
)的代码实现:
1 | public class ConcreteHandler extends Handler { |
职责链模式可分为纯的职责链模式和不纯的职责链模式两种:
- 纯的职责链模式
一个纯的职责链模式要求一个具体处理者对象只能在两个行为中选择一个:要么承担全部责任,要么将责任推给下家,不允许出现某一个具体处理者对象在承担了一部分或全部责任后又将责任向下传递的情况。而且在纯的职责链模式中,要求一个请求必须被某一个处理者对象所接收,不能出现某个请求未被任何一个处理者对象处理的情况。在前面的采购单审批实例中应用的是纯的职责链模式。
- 不纯的职责链模式
在一个不纯的职责链模式中允许某个请求被一个具体处理者部分处理后再向下传递,或者一个具体处理者处理完某请求后其后继处理者可以继续处理该请求,而且一个请求可以最终不被任何处理者对象所接收。
职责链模式
的主要优点如下:
职责链模式
使得一个对象无须知道是其他哪一个对象处理其请求,对象仅需知道该请求会被处理即可,接收者和发送者都没有对方的明确信息,且链中的对象不需要知道链的结构,由客户端负责链的创建,降低了系统的耦合度- 请求处理对象仅需维持一个指向其后继者的引用,而不需要维持它对所有的候选处理者的引用,可简化对象的相互连接
- 在给对象分派职责时,职责链可以给我们更多的灵活性,可以通过在运行时对该链进行动态的增加或修改来增加或改变处理一个请求的职责
- 在系统中增加一个新的具体请求处理者时无须修改原有系统的代码,只需要在客户端重新建链即可,从这一点来看是符合
开闭原则
的
职责链模式
的主要缺点如下:
- 由于一个请求没有明确的接收者,那么就不能保证它一定会被处理,该请求可能一直到链的末端都得不到处理;一个请求也可能因职责链没有被正确配置而得不到处理
- 对于比较长的职责链,请求的处理可能涉及到多个处理对象,系统性能将受到一定影响,而且在进行代码调试时不太方便
- 如果建链不当,可能会造成循环调用,将导致系统陷入死循环
责任链模式
的使用场景如下:
- 有多个对象可以处理同一个请求,具体哪个对象处理该请求待运行时刻再确定,客户端只需将请求提交到链上,而无须关心请求的处理对象是谁以及它是如何处理的
- 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求
- 可动态指定一组对象处理请求,客户端可以动态创建职责链来处理请求,还可以改变链中处理者之间的先后次序
命令模式
意图: 将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化
解决的问题: 在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适
角色: 命令模式
中有如下角色:
- 抽象命令类(
Command
):抽象命令类一般是一个抽象类或接口,在其中声明了用于执行请求的execute()
等方法,通过这些方法可以调用请求接收者的相关操作 - 具体命令类(
ConcreteCommand
):具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中。在实现execute()
方法时,将调用接收者对象的相关操作 - 调用者(
Invoker
):调用者即请求发送者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类之间存在关联关系。在程序运行时可以将一个具体命令对象注入其中,再调用具体命令对象的execute()
方法,从而实现间接调用请求接收者的相关操作 - 接收者(
Receiver
):接收者执行与请求相关的操作,它具体实现对请求的业务处理
命令模式
(Command Pattern
)将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式。命令模式
中各个角色的代码实现如下:
抽象命令类(Command
)的代码实现:
1 | public interface Command { |
具体命令类(ConcreteCommand
)的代码实现:
1 | public class ConcreteCommand implements Command { |
调用者(Invoker
)的代码实现:
1 | public class Invoker { |
接收者(Receiver
)的代码实现:
1 | public class Receiver { |
调用方式如下:
1 | public class Client { |
命令模式
的主要优点如下:
- 降低系统的耦合度。由于请求者与接收者之间不存在直接引用,因此请求者与接收者之间实现完全解耦,相同的请求者可以对应不同的接收者,同样,相同的接收者也可以供不同的请求者使用,两者之间具有良好的独立性
- 新的命令可以很容易地加入到系统中。由于增加新的具体命令类不会影响到其他类,因此增加新的具体命令类很容易,无须修改原有系统源代码,甚至客户类代码,满足
开闭原则
的要求 - 可以比较容易地设计一个命令队列或宏命令(组合命令)
- 为请求的撤销和恢复操作提供了一种设计和实现方案
命令模式
的主要缺点如下:
使用命令模式
可能会导致某些系统有过多的具体命令类。因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类,因此在某些系统中可能需要提供大量的具体命令类,这将影响命令模式的使用。
命令模式
的使用场景如下:
- 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。请求调用者无须知道接收者的存在,也无须知道接收者是谁,接收者也无须关心何时被调用
- 系统需要在不同的时间指定请求、将请求排队和执行请求。一个命令对象和请求的初始调用者可以有不同的生命期,换言之,最初的请求发出者可能已经不在了,而命令对象本身仍然是活动的,可以通过该命令对象去调用请求接收者,而无须关心请求调用者的存在性,可以通过请求日志文件等机制来具体实现
- 系统需要支持命令的撤销操作和恢复操作。
- 系统需要将一组操作组合在一起形成宏命令
解释器模式
意图: 给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子
解决的问题: 对于一些固定文法构建一个解释句子的解释器
角色: 解释器模式
中存在如下角色:
- 抽象表达式(
AbstractExpression
):在抽象表达式中声明了抽象的解释操作,它是所有终结符表达式和非终结符表达式的公共父类 - 终结符表达式(
TerminalExpression
):终结符表达式是抽象表达式的子类,它实现了与文法中的终结符相关联的解释操作,在句子中的每一个终结符都是该类的一个实例。通常在一个解释器模式中只有少数几个终结符表达式类,它们的实例可以通过非终结符表达式组成较为复杂的句子 - 非终结符表达式(
NonterminalExpression
):非终结符表达式也是抽象表达式的子类,它实现了文法中非终结符的解释操作,由于在非终结符表达式中可以包含终结符表达式,也可以继续包含非终结符表达式,因此其解释操作一般通过递归的方式来完成 - 环境类(
Context
):环境类又称为上下文类,它用于存储解释器之外的一些全局信息,通常它临时存储了需要解释的语句
解释器模式
(Interpreter Pattern
)定义一个语言的文法,并且建立一个解释器来解释该语言中的句子,这里的语言
是指使用规定格式和语法的代码。解释器模式是一种类行为型模式。解释器模式
中各个角色的代码实现如下:
抽象表达式(AbstractExpression
)的代码实现:
1 | public abstract class AbstractExpression { |
终结符表达式(TerminalExpression
)的代码实现:
1 | public class TerminalExpression extends AbstractExpression { |
非终结符表达式(NonterminalExpression
)的代码实现:
1 | public class NonterminalExpression extends AbstractExpression { |
环境类(Context
)的代码实现:
1 | public class Context { |
调用方法如下:
1 | public class Client { |
解释器模式
的主要优点如下:
- 易于改变和扩展文法。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法
- 每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言
- 实现文法较为容易。在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂,还可以通过一些工具自动生成节点类代码
- 增加新的解释表达式较为方便。如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合
开闭原则
解释器模式
的主要缺点如下:
- 对于复杂文法难以维护。在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护,此时可以考虑使用语法分析程序等方式来取代解释器模式
- 执行效率较低。由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦
解释器模式
适用于以下场景:
- 可以将一个需要解释执行的语言中的句子表示为一个抽象语法树
- 一些重复出现的问题可以用一种简单的语言来进行表达
- 一个语言的文法较为简单
- 执行效率不是关键问题
迭代器模式
意图: 提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部表示
解决的问题: 不同的方式来遍历整个整合对象
角色: 迭代器模式
中有如下角色:
- 抽象迭代器(
Iterator
):它定义了访问和遍历元素的接口,声明了用于遍历数据元素的方法,例如:用于获取第一个元素的first()
方法,用于访问下一个元素的next()
方法,用于判断是否还有下一个元素的hasNext()
方法,用于获取当前元素的currentItem()
方法等,在具体迭代器中将实现这些方法 - 具体迭代器(
ConcreteIterator
):它实现了抽象迭代器接口,完成对聚合对象的遍历,同时在具体迭代器中通过游标来记录在聚合对象中所处的当前位置,在具体实现时,游标通常是一个表示位置的非负整数 - 抽象聚合类(
Aggregate
):它用于存储和管理元素对象,声明一个createIterator()
方法用于创建一个迭代器对象,充当抽象迭代器工厂角色 - 具体聚合类(
ConcreteAggregate
):它实现了在抽象聚合类中声明的createIterator()
方法,该方法返回一个与该具体聚合类对应的具体迭代器ConcreteIterator
实例
迭代器模式
(Iterator Pattern
)提供一种方法来访问聚合对象,而不用暴露这个对象的内部表示,其别名为游标(Cursor
)。迭代器模式是一种对象行为型模式。迭代器模式
中各个角色的代码实现如下:
抽象迭代器(Iterator
)的代码实现:
1 | public interface Iterator { |
具体迭代器(ConcreteIterator
)的代码实现:
1 | public class ConcreteIterator implements Iterator { |
抽象聚合类(Aggregate
)的代码实现:
1 | public abstract class Aggregate { |
具体聚合类(ConcreteAggregate
)的代码实现:
1 | public class ConcreteAggregate extends Aggregate { |
调用方式:
1 | public class Client { |
迭代器模式
的主要优点如下:
- 它支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式。在迭代器模式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,我们也可以自己定义迭代器的子类以支持新的遍历方式
- 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方法,这样可以简化聚合类的设计
- 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,满足
开闭原则
的要求
迭代器模式
的主要缺点如下:
- 由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性
- 抽象迭代器的设计难度较大,需要充分考虑到系统将来的扩展,例如
JDK
内置迭代器Iterator
就无法实现逆向遍历,如果需要实现逆向遍历,只能通过其子类ListIterator
等来实现,而ListIterator
迭代器无法用于操作Set
类型的聚合对象。在自定义迭代器时,创建一个考虑全面的抽象迭代器并不是件很容易的事情
迭代器模式
的使用场景:
- 访问一个聚合对象的内容而无须暴露它的内部表示。将聚合对象的访问与内部数据的存储分离,使得访问聚合对象时无须了解其内部实现细节
- 需要为一个聚合对象提供多种遍历方式
- 为遍历不同的聚合结构提供一个统一的接口,在该接口的实现类中为不同的聚合结构提供不同的遍历方式,而客户端可以一致性地操作该接口
中介者模式
意图: 用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互
解决的问题: 对象与对象之间存在大量的关联关系,这样势必会导致系统的结构变得很复杂,同时若一个对象发生改变,我们也需要跟踪与之相关联的对象,同时做出相应的处理
角色: 中介者模式
中有如下角色:
- 抽象中介者(
Mediator
):它定义一个接口,该接口用于与各同事对象之间进行通信 - 具体中介者(
ConcreteMediator
):它是抽象中介者的子类,通过协调各个同事对象来实现协作行为,它维持了对各个同事对象的引用 - 抽象同事类(
Colleague
):它定义各个同事类公有的方法,并声明了一些抽象方法来供子类实现,同时它维持了一个对抽象中介者类的引用,其子类可以通过该引用来与中介者通信 - 具体同事类(
ConcreteColleague
):它是抽象同事类的子类;每一个同事对象在需要和其他同事对象通信时,先与中介者通信,通过中介者来间接完成与其他同事类的通信;在具体同事类中实现了在抽象同事类中声明的抽象方法
中介者模式
(Mediator Pattern
):用一个中介对象(中介者)来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。中介者模式又称为调停者模式,它是一种对象行为型模式。中介者模式
中各个角色的代码实现如下:
抽象中介者(Mediator
)的代码实现:
1 | public abstract class Mediator { |
具体中介者(ConcreteMediator
)的代码实现:
1 | public class ConcreteMediator extends Mediator { |
抽象同事类(Colleague
)的代码实现:
1 | public abstract class Colleague { |
具体同事类(ConcreteColleague
)的代码实现:
1 | public class ColleagueA extends Colleague { |
调用代码:
1 | public class Client { |
中介者模式
的主要优点如下:
中介者模式
简化了对象之间的交互,它用中介者和同事的一对多交互代替了原来同事之间的多对多交互,一对多关系更容易理解、维护和扩展,将原本难以理解的网状结构转换成相对简单的星型结构中介者模式
可将各同事对象解耦。中介者有利于各同事之间的松耦合,我们可以独立的改变和复用每一个同事和中介者,增加新的中介者和新的同事类都比较方便,更好地符合开闭原则
- 可以减少子类生成,中介者将原本分布于多个对象间的行为集中在一起,改变这些行为只需生成新的中介者子类即可,这使各个同事类可被重用,无须对同事类进行扩展
中介者模式
的主要缺点如下:
在具体中介者类中包含了大量同事之间的交互细节,可能会导致具体中介者类非常复杂,使得系统难以维护。
中介者模式
使用场景如下:
- 系统中对象之间存在复杂的引用关系,系统结构混乱且难以理解
- 一个对象由于引用了其他很多对象并且直接和这些对象通信,导致难以复用该对象
- 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。可以通过引入中介者类来实现,在中介者中定义对象交互的公共行为,如果需要改变行为则可以增加新的具体中介者类
备忘录模式
意图: 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态
解决的问题: 所谓备忘录模式就是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态
角色: 备忘录模式
中有如下角色:
- 原发器(
Originator
):它是一个普通类,可以创建一个备忘录,并存储它的当前内部状态,也可以使用备忘录来恢复其内部状态,一般将需要保存内部状态的类设计为原发器 - 备忘录(
Memento
):存储原发器的内部状态,根据原发器来决定保存哪些内部状态。备忘录的设计一般可以参考原发器的设计,根据实际需要确定备忘录类中的属性。需要注意的是,除了原发器本身与负责人类之外,备忘录对象不能直接供其他类使用,原发器的设计在不同的编程语言中实现机制会有所不同 - 负责人(
Caretaker
):负责人又称为管理者,它负责保存备忘录,但是不能对备忘录的内容进行操作或检查。在负责人类中可以存储一个或多个备忘录对象,它只负责存储对象,而不能修改对象,也无须知道对象的实现细节
备忘录模式
(Memento Pattern
):在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。它是一种对象行为型模式,其别名为 Token
。备忘录模式
中各个角色的代码实现如下:
原发器(Originator
)的代码实现:
1 | public class Originator { |
备忘录(Memento
)的代码实现:
1 | public class Memento { |
负责人(Caretaker
)的代码实现:
1 | public class Caretaker { |
调用代码:
1 | public class Client { |
备忘录模式
的主要优点如下:
- 它提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原
- 备忘录实现了对信息的封装,一个备忘录对象是一种原发器对象状态的表示,不会被其他代码所改动。备忘录保存了原发器的状态,采用列表、堆栈等集合来存储备忘录对象可以实现多次撤销操作
备忘录模式
的主要缺点如下:
资源消耗过大,如果需要保存的原发器类的成员变量太多,就不可避免需要占用大量的存储空间,每保存一次对象的状态都需要消耗一定的系统资源。
备忘录模式
的主要使用场景:
- 保存一个对象在某一个时刻的全部状态或部分状态,这样以后需要时它能够恢复到先前的状态,实现撤销操作
- 防止外界对象破坏一个对象历史状态的封装性,避免将对象历史状态的实现细节暴露给外界对象
观察者模式
意图: 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新
解决的问题: 一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作
角色: 观察者模式
中有如下角色:
- 目标(
Subject
):目标又称为主题,它是指被观察的对象。在目标中定义了一个观察者集合,一个观察目标可以接受任意数量的观察者来观察,它提供一系列方法来增加和删除观察者对象,同时它定义了通知方法notify()
。目标类可以是接口,也可以是抽象类或具体类 - 具体目标(
ConcreteSubject
):具体目标是目标类的子类,通常它包含有经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知;同时它还实现了在目标类中定义的抽象业务逻辑方法(如果有的话)。如果无须扩展目标类,则具体目标类可以省略 - 观察者(
Observer
):观察者将对观察目标的改变做出反应,观察者一般定义为接口,该接口声明了更新数据的方法update()
,因此又称为抽象观察者 - 具体观察者(
ConcreteObserver
):在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者Observer
中定义的update()
方法。通常在实现时,可以调用具体目标类的attach()
方法将自己添加到目标类的集合中或通过detach()
方法将自己从目标类的集合中删除
观察者模式
(Observer Pattern
):定义对象之间的一种一对多的依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式是一种对象行为型模式。观察者模式
中各个角色的代码实现如下:
目标(Subject
)的代码实现:
1 | public interface Subject { |
具体目标(ConcreteSubject
)的代码实现:
1 | public class ConcreteSubject implements Subject { |
观察者(Observer
)的代码实现:
1 | public interface Observer { |
具体观察者(ConcreteObserver
)的代码实现:
1 | public class ConcrereObserver implements Observer { |
调用者代码:
1 | public class Client { |
观察者模式
的主要优点如下:
观察者模式
可以实现表示层和数据逻辑层的分离,定义了稳定的消息更新传递机制,并抽象了更新接口,使得可以有各种各样不同的表示层充当具体观察者角色观察者模式
在观察目标和观察者之间建立一个抽象的耦合。观察目标只需要维持一个抽象观察者的集合,无须了解其具体观察者。由于观察目标和观察者没有紧密地耦合在一起,因此它们可以属于不同的抽象化层次观察者模式
支持广播通信,观察目标会向所有已注册的观察者对象发送通知,简化了一对多系统设计的难度观察者模式
符合开闭原则
的要求,增加新的具体观察者无须修改原有系统代码,在具体观察者与观察目标之间不存在关联关系的情况下,增加新的观察目标也很方便
观察者模式
的主要缺点如下:
- 如果一个观察目标对象有很多直接和间接观察者,将所有的观察者都通知到会花费很多时间
- 如果在观察者和观察目标之间存在循环依赖,观察目标会触发它们之间进行循环调用,可能导致系统崩溃
观察者模式
没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化
观察者模式
的使用场景如下:
- 一个抽象模型有两个方面,其中一个方面依赖于另一个方面,将这两个方面封装在独立的对象中使它们可以各自独立地改变和复用
- 一个对象的改变将导致一个或多个其他对象也发生改变,而并不知道具体有多少对象将发生改变,也不知道这些对象是谁
- 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制
状态模式
意图: 允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类
解决的问题: 对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为
角色: 状态模式
中有如下角色:
- 环境类(
Context
):环境类又称为上下文类,它是拥有多种状态的对象。由于环境类的状态存在多样性且在不同状态下对象的行为有所不同,因此将状态独立出去形成单独的状态类。在环境类中维护一个抽象状态类State
的实例,这个实例定义当前状态,在具体实现时,它是一个State
子类的对象 - 抽象状态类(
State
):它用于定义一个接口以封装与环境类的一个特定状态相关的行为,在抽象状态类中声明了各种不同状态对应的方法,而在其子类中实现类这些方法,由于不同状态下对象的行为可能不同,因此在不同子类中方法的实现可能存在不同,相同的方法可以写在抽象状态类中 - 具体状态类(
ConcreteState
):它是抽象状态类的子类,每一个子类实现一个与环境类的一个状态相关的行为,每一个具体状态类对应环境的一个具体状态,不同的具体状态类其行为有所不同
状态模式
(State Pattern
)允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。状态模式是一种对象行为型模式。状态模式
中各个角色的代码实现如下:
环境类(Context
)的代码实现:
1 | public class Context { |
抽象状态类(State
)的代码实现:
1 | public interface State { |
具体状态类(ConcreteState
)的代码实现:
1 | public class ConcreteStateA implements State { |
1 | public class ConcreteStateB implements State { |
调用代码:
1 | public class Client { |
状态模式
的主要优点如下:
- 封装了状态的转换规则,在状态模式中可以将状态的转换代码封装在环境类或者具体状态类中,可以对状态转换代码进行集中管理,而不是分散在一个个业务方法中
- 将所有与某个状态有关的行为放到一个类中,只需要注入一个不同的状态对象即可使环境对象拥有不同的行为
- 允许状态转换逻辑与状态对象合成一体,而不是提供一个巨大的条件语句块,状态模式可以让我们避免使用庞大的条件语句来将业务方法和状态转换代码交织在一起
- 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数
状态模式
的主要缺点如下:
- 状态模式的使用必然会增加系统中类和对象的个数,导致系统运行开销增大
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱,增加系统设计的难度
- 状态模式对
开闭原则
的支持并不太好,增加新的状态类需要修改那些负责状态转换的源代码,否则无法转换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码
状态模式
的使用场景如下:
- 对象的行为依赖于它的状态(如某些属性值),状态的改变将导致行为的变化
- 在代码中包含大量与对象状态有关的条件语句,这些条件语句的出现,会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,并且导致客户类与类库之间的耦合增强
策略模式
意图: 定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换
解决的问题: 在有多种算法相似的情况下,使用 if...else
所带来的复杂和难以维护
角色: 策略模式
有如下角色:
- 环境类(
Context
):环境类是使用算法的角色,它在解决某个问题(即实现某个方法)时可以采用多种策略。在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略 - 抽象策略类(
Strategy
):它为所支持的算法声明了抽象方法,是所有策略类的父类,它可以是抽象类或具体类,也可以是接口。环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法 - 具体策略类(
ConcreteStrategy
):它实现了在抽象策略类中声明的算法,在运行时,具体策略类将覆盖在环境类中定义的抽象策略类对象,使用一种具体的算法实现某个业务处理
策略模式
(Strategy Pattern
)定义一系列算法类,将每一个算法封装起来,并让它们可以相互替换,策略模式让算法独立于使用它的客户而变化。策略模式是一种对象行为型模式。策略模式
中各个角色的代码实现如下:
环境类(Context
)的代码实现:
1 | public class Context { |
抽象策略类(Strategy
)的代码实现:
1 | public interface Strategy { |
具体策略类(ConcreteStrategy
)的代码实现:
1 | public class ConcreteStrategyA implements Strategy { |
1 | public class ConcreteStrategyB implements Strategy { |
调用代码:
1 | public class Client { |
策略模式
的主要优点如下:
策略模式
提供了对开闭原则
的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为策略模式
提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族,恰当使用继承可以把公共的代码移到抽象策略类中,从而避免重复的代码策略模式
提供了一种可以替换继承关系的办法。如果不使用策略模式,那么使用算法的环境类就可能会有一些子类,每一个子类提供一种不同的算法。但是,这样一来算法的使用就和算法本身混在一起,不符合单一职责原则
,决定使用哪一种算法的逻辑和该算法本身混合在一起,从而不可能再独立演化;而且使用继承无法实现算法或行为在程序运行时的动态切换- 使用
策略模式
可以避免多重条件选择语句。多重条件选择语句不易维护,它把采取哪一种算法或行为的逻辑与算法或行为本身的实现逻辑混合在一起,将它们全部硬编码(Hard Coding
)在一个庞大的多重条件选择语句中,比直接继承环境类的办法还要原始和落后 策略模式
提供了一种算法的复用机制,由于将算法单独提取出来封装在策略类中,因此不同的环境类可以方便地复用这些策略类
策略模式
的主要缺点如下:
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法。换言之,策略模式只适用于客户端知道所有的算法或行为的情况
- 策略模式将造成系统产生很多具体策略类,任何细小的变化都将导致系统要增加一个新的具体策略类
- 无法同时在客户端使用多个策略类,也就是说,在使用策略模式时,客户端每次只能使用一个策略类,不支持使用一个策略类完成部分功能后再使用另一个策略类来完成剩余功能的情况
策略模式
的使用场景如下:
- 一个系统需要动态地在几种算法中选择一种,那么可以将这些算法封装到一个个的具体算法类中,而这些具体算法类都是一个抽象算法类的子类。换言之,这些具体算法类均有统一的接口,根据
里氏代换原则
和面向对象的多态性,客户端可以选择使用任何一个具体算法类,并只需要维持一个数据类型是抽象算法类的对象 - 一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重条件选择语句来实现。此时,使用策略模式,把这些行为转移到相应的具体策略类里面,就可以避免使用难以维护的多重条件选择语句
- 不希望客户端知道复杂的、与算法相关的数据结构,在具体策略类中封装算法与相关的数据结构,可以提高算法的保密性与安全性
模板方法模式
意图: 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤
解决的问题: 一些方法通用,却在每一个子类都重新写了这一方法
角色: 模板方法模式
中有如下角色:
- 抽象类(
AbstractClass
):在抽象类中定义了一系列基本操作(Primitive Operations
),这些基本操作可以是具体的,也可以是抽象的,每一个基本操作对应算法的一个步骤,在其子类中可以重定义或实现这些步骤。同时,在抽象类中实现了一个模板方法(Template Method
),用于定义一个算法的框架,模板方法不仅可以调用在抽象类中实现的基本方法,也可以调用在抽象类的子类中实现的基本方法,还可以调用其他对象中的方法 - 具体子类(
ConcreteClass
):它是抽象类的子类,用于实现在父类中声明的抽象基本操作以完成子类特定算法的步骤,也可以覆盖在父类中已经实现的具体基本操作
模板方法模式
(Template Method Pattern
)定义一个操作中算法的框架,而将一些步骤延迟到子类中。模板方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。模板方法模式
中各个角色的代码实现如下:
抽象类(AbstractClass
)的代码实现;
1 | public abstract class AbstractClass { |
具体子类(ConcreteClass
)的代码实现:
1 | public class ConcreteClassA extends AbstractClass { |
1 | public class ConcreteClassB extends AbstractClass { |
调用者代码:
1 | public class Client { |
模板方法模式
的主要优点如下:
- 在父类中形式化地定义一个算法,而由它的子类来实现细节的处理,在子类实现详细的处理算法时并不会改变算法中步骤的执行次序
- 模板方法模式是一种代码复用技术,它在类库设计中尤为重要,它提取了类库中的公共行为,将公共行为放在父类中,而通过其子类来实现不同的行为,它鼓励我们恰当使用继承来实现代码复用
- 可实现一种反向控制结构,通过子类覆盖父类的钩子方法来决定某一特定步骤是否需要执行
- 在模板方法模式中可以通过子类来覆盖父类的基本方法,不同的子类可以提供基本方法的不同实现,更换和增加新的子类很方便,符合
单一职责原则
和开闭原则
模板方法模式
的主要缺点如下:
需要为每一个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统更加庞大,设计也更加抽象,此时,可结合桥接模式来进行设计。
模板方法模式
的使用场景:
- 对一些复杂的算法进行分割,将其算法中固定不变的部分设计为模板方法和父类具体方法,而一些可以改变的细节由其子类来实现。即:一次性实现一个算法的不变部分,并将可变的行为留给子类来实现
- 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复
- 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制
访问者模式
意图: 主要将数据结构与数据操作分离
解决的问题: 稳定的数据结构和易变的操作耦合问题
角色: 访问者模式
中有如下角色:
- 抽象访问者(
Vistor
):抽象访问者为对象结构中每一个具体元素类ConcreteElement
声明一个访问操作,从这个操作的名称或参数类型可以清楚知道需要访问的具体元素的类型,具体访问者需要实现这些操作方法,定义对这些元素的访问操作 - 具体访问者(
ConcreteVisitor
):具体访问者实现了每个由抽象访问者声明的操作,每一个操作用于访问对象结构中一种类型的元素 - 抽象元素(
Element
):抽象元素一般是抽象类或者接口,它定义一个accept()
方法,该方法通常以一个抽象访问者作为参数 - 具体元素(
ConcreteElement
):具体元素实现了accept()
方法,在accept()
方法中调用访问者的访问方法以便完成对一个元素的操作 - 对象结构(
ObjectStructure
):对象结构是一个元素的集合,它用于存放元素对象,并且提供了遍历其内部元素的方法。它可以结合组合模式来实现,也可以是一个简单的集合对象,如一个List
对象或一个Set
对象
访问者模式
(Visitor Pattern
):提供一个作用于某对象结构中的各元素的操作表示,它使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。访问者模式是一种对象行为型模式。访问者模式
中各个角色的代码实现如下:
抽象访问者(Vistor
)的代码实现:
1 | public interface Visitor { |
具体访问者(ConcreteVisitor
)的代码实现:
1 | public class ConcreteVisitorA implements Visitor { |
抽象元素(Element
)的代码实现:
1 | public interface Element { |
具体元素(ConcreteElement
)的代码实现:
1 | public class ConcreteElementA implements Element { |
1 | public class ConcreteElementB implements Element { |
调用者代码实现:
1 | public class Client { |
访问者模式
的主要优点如下:
- 增加新的访问操作很方便。使用
访问者模式
,增加新的访问操作就意味着增加一个新的具体访问者类,实现简单,无须修改源代码,符合开闭原则
- 将有关元素对象的访问行为集中到一个访问者对象中,而不是分散在一个个的元素类中。类的职责更加清晰,有利于对象结构中元素对象的复用,相同的对象结构可以供多个不同的访问者访问
- 让用户能够在不修改现有元素类层次结构的情况下,定义作用于该层次结构的操作
访问者模式
的主要缺点如下:
- 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作,这违背了
开闭原则
的要求 - 破坏封装。
访问者模式
要求访问者对象访问并调用每一个元素对象的操作,这意味着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法供访问者访问
访问模式
的使用场景如下:
- 一个对象结构包含多个类型的对象,希望对这些对象实施一些依赖其具体类型的操作。在访问者中针对每一种具体的类型都提供了一个访问操作,不同类型的对象可以有不同的访问操作
- 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作
污染
这些对象的类,也不希望在增加新操作时修改这些类。访问者模式使得我们可以将相关的访问操作集中起来定义在访问者类中,对象结构可以被多个不同的访问者类所使用,将对象本身与对象的访问操作分离 - 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作