Java 设计模式学习笔记

这学期上了设计模式的专业课,感觉是迄今在校上过的对自己代码能力提升最有帮助的专业课了。感谢开设这门课的王老师——在大学生涯中碰到的为数不多对教学充满热情、认真对待的老师之一,让我领略到设计模式的魅力。

笔记的目录结构参考了 B 站上李建忠老师的 C++ 设计模式视频中所讲授的顺序。除了第一二三章外,绝大部分文字和图片摘录于本学期老师指定的设计模式教材 《Java 设计模式》以及配套的 PPT 教学课件。

个人不建议没接触过设计模式的直接从李建忠老师的视频入门,李老师的课程更适合之前学过设计模式的人用来复习。前两节课入门听一听还是可以的,到后面举例和代码越来越少,没有实际的应用场景很难真正理解模式的真正意义。可以先看看其他书籍或视频,多写一些代码,再来看这个就别有一番滋味了。书籍推荐上面提到的王伟的《Java 设计模式》,书中有很好的设计模式应用场景举例,配合给的 Java 代码实现,理解起来轻松多了。网上有免费的电子版,感兴趣的可以去搜搜看。

介绍

设计模式:前人总结的可以解决特定问题的一套模式也就是模版。

每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动。——Christopher Alexander

历史性著作《设计模式:可复用面向对象软件的基础》一书中描述了23种经典面向对象设计模式,创立了模式在软件设计中的地位。

由于《设计模式》一书确定了设计模式的地位,通常所说的设计模式隐含地表示“面向对象设计模式”。但这并不意味“设计模式”就等于“面向对象设计模式”。

编程的两种思维模式:

  • 底层思维:向下,如何把握机器底层从微观理解对象构造,关注于语言构造、编译转换、内存模型、运行时机制
  • 抽象思维:向上,如何将我们的周围世界抽象为程序代码,面向对象、组件封装、设计模式、架构模式。

软件设计复杂的根本原因是要面对各种各样的变化:客户需求的变化、技术平台的变化、开发团队的变化、市场环境的变化。

建筑商从来不会去想给一栋已建好的 100 层高的楼房底下再新修一个小地下室——这样做花费极大而且注定要失败。然而令人惊奇的是,软件系统的用户在要求作出类似改变时却不会仔细考虑,而且他们认为这只是需要简单编程的事。——《Object-Oriented Analysis and Design with Applications》

解决复杂性通常有两种思路:

  • 分解:人们面对复杂性有一个常见的做法:即分而治之,将大问题分解为多个小问题,将复杂问题分解为多个简单问题。例如常见的结构化设计语言:C 语言。分而治之不容易复用,但出现新的变化时针往往要修改已有的代码来应对变化。
  • 抽象:更高层次来讲,人们处理复杂性有一个通用的技术,即抽象。由于不能掌握全部的复杂对象,我们选择忽视它的非本质细节,而去处理泛化和理想化了的对象模型。即忽略具体实现,归纳总结提取一类事物的共性。

使用了面向对象的机制并不代表就是一个好的面向对象设计,好的软件设计的标准是复用,复用性越高,说明越是一个好的软件设计。所谓“好的面向对象设计”指是那些可以满足 “应对变化,提高复用”的设计 。

面向对象设计

面向对象思想

对象是什么?

  • 从语言实现层面来看,对象封装了代码和数据。
  • 从规格层面讲,对象是一系列可被使用的公共接口。
  • 从概念层面讲,对象是某种拥有责任的抽象。

面向对象三大机制:

  • 封装(Encapsulation):隐藏内部实现,将对象的状态和行为捆绑到一个单一逻辑单元机制。封装并不是面向对象语言(OOPL)独有, 但面向对象的封装更加完美。封装代码和数据的本质是封装变化点,使得一侧变化一侧稳定。使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合。
  • 继承(Inheritance):复用现有代码,子类自动共享父类数据结构和方法,通过继承可以不加修改地重用和扩展已经测试的代码。即使没有已有类的源代码,仍然可以从已有的类中派生出新类。编译单位级别的复用而不是源代码片段级的复用可以显著地减少代码冗余,提高代码质量,减轻维护代码的负担。
  • 多态(Polymorphism):改写对象行为,允许不同类的对象对同一消息做出响应,即同一消息可以根据发送对象的不同而采用多种不同的行为方式。

类重用的方式

类间的六种重用方法的耦合性由弱到强分别是:依赖 < (普通)关联 < 聚合 < 组合 < 实现 = 泛化。优先使用耦合度较低的依赖和关联,少用继承。在 UML 类图中的六种关系的表示如下:

img

下图对其中的五种类关系进行了举例,更详细的说明可以移步我的另一篇博客:UML 中的类图

image-20201213084132558

面向对象设计原则

单一职责原则(Single Responsibility Principle,SRP)

  • 一个类应该仅有一个引起它变化的原因,变化的方向隐含着类的责任。
  • 一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。

开放封闭原则(Open-Closed Principle,OCP)

  • 对扩展开放,对更改封闭。
  • 类模块应该是可扩展的,但是不可修改。
  • 需求变更时,不应该尝试去更改已有的模块,而是去增加一些模块来应对变化。

里氏替换原则(Liskov Substitution Principle,LSP)

  • 所有引用基类的地方必须能透明地使用其子类的对象。
  • 子类必须能够替换它们的基类(is-a)。
  • 继承表达类型抽象。
  • 如果继承是为扩展,子类只能通过新添加方法来扩展功能,而不是覆写父类中的具体方法,否则当子类对象将父类对象替换掉时,程序逻辑可能发生改变。
  • 如果继承是为了多态,不要从可实例化的父类中继承,而是要使用基于抽象类和接口的继承,这样父类是不可实例化的,也就不存在子类替换父类实例(根本不存在父类实例了)时逻辑不一致的可能。

依赖倒置原则(Dependence Inversion Principle,DIP)

  • 高层模块(稳定)不应该依赖于低层模块(变化),二者都应该依赖于抽象(稳定) 。高层模块应该是稳定的,底层模块是容易根据需求功能发生变化的,高层模块如果依赖于变化的底层模块,则高层模块也变得不稳定,要经常去修改。
    image-20200918222855319

  • 抽象(稳定)不应该依赖于实现细节(变化) ,实现细节应该依赖于抽象(稳定)。即在抽象类中不应该使用和子类细节相关的属性和方法。

  • 针对抽象层(接口或抽象类)编程,而不是针对实现编程。在函数的形参列表中或者关联关系中尽量引用抽象类。即不将变量类型声明为某个特定的具体类,而是声明为某个接口或者抽象类。客户程序无需获知对象的具体类型,只需要知道对象所具有的接口。具体类的对象实例通过依赖注入(Dependence Injection)的方式注入到其他对象的函数中,具体可以分为构造注入、设值注入、接口注入。

  • 开闭原则是目标,里氏替换原则是基础,依赖倒转原则是手段。

接口隔离原则(Interface Segregation Principle,ISP)

  • 不应该强迫客户程序依赖它们不用的方法。应该使用适当的修饰符控制接口中方法和变量的可见性,使用该接口的客户端仅需知道与之相关的方法和变量即可。
  • 接口应该小而完备。当一个接口太大时,需要将它分割成一些更细小的接口;每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干。
  • 接口标准化是软件从其他行业,例如制造业,建筑业借鉴过来的经验,接口的标准化,也是一个行业成熟强盛的标志。

合成复用原则(Composite Reuse Principle,CRP)

  • 优先使用对象组合达到复用的目的,而不是类继承。
  • 类继承通常为“白箱复用”,对象组合通常为“黑箱复用”。
  • 继承在某种程度上破坏了封装性,子类父类耦合度高。而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低。
  • 继承是类属关系,组合是对象关系,只有在两个类直接具有明确的类属关系的时候才能用继承。

迪米特法则(Law of Demeter,LoD)

  • 每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。又称为最少知识原则(Least Knowledge Principle, LKP)
  • 不要和陌生人说话,只与朋友(密切相关的对象)通信,
  • 应当尽可能少地与其他实体发生相互作用,减少对象之间的交互,不必通信则不应该通信,如果必须通信,可以通过第三方转发通信,降低耦合。
  • 减少系统中各部分的依赖关系,从而实现“高内聚、松耦合”的类型设计方案,耦合度越低,越利于复用。

三种粒度的软件设计经验

  • 设计习语(Design Idioms):描述与特定编程语言相关的低层模式,技巧,惯用法。
  • 设计模式(Design Patterns):主要描述的是“类与相互通信的对象之间的组织关系,包括它们的角色、职责、协作方式等方面。
  • 架构模式(Architectural Patterns):描述系统中与基本结构组织关系密切的高层模式,包括子系统划分,职责,以及如何组织它们之间关系的规则。

设计模式解决的是设计思想的复用性,并不针对具体的编程语言。软件体系结构则是构件层面的复用,粒度更大。

设计模式的定义与分类

设计模式定义

GoF(Gang of Four,Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides,四位作者的被合称为”四人组“)1995 年在他们的著作《设计模式:可复用面向对象软件的基础》中对设计模式是这样定义的:设计模式是在特定环境下为解决某一通用软件设计问题提供的一套定制的解决方案,该方案描述了对象和类之间的相互作用。

Design patterns are descriptions of communicating objects and classes that are customized to solve a general design problem in a particular context.——《Design Patterns: Elements of Reusable Object-Oriented Software》

设计模式也可以理解为设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。简单说就是前人总结的可以解决特定问题的一套模式也就是模版。

设计模式的基本要素:

  • 模式名称 (Pattern Name)
  • 问题 (Problem)
  • 解决方案 (Solution)
  • 效果 (Consequences)

模式分类

设计模式传统按照目的和范围分类。

根据目的划分:

  • 创建型(Creational)模式:将对象的部分创建工作延迟到子类或者其他对象,从而应对需求变化为对象创建时具体类型实现引来的冲击。
  • 结构型(Structural)模式:通过类继承或者对象组合获得更灵活的结构,从而应对需求变化为对象的结构带来的冲击。
  • 行为型(Behavioral)模式:通过类继承或者对象组合来划分类与对象间的职责,从而应对需求变化为多个交互的对象带来的冲击。

根据范围划分:

  • 类模式:处理类和子类之间的静态关系,这些关系通过继承建立,在编译时刻就被确定下来,是一种静态关系
  • 对象模式:处理对象间的动态关系,这些关系在运行时变化,更具动态性

image-20201213090054431

李建忠老师提出一种从封装变化角度对模式分类的方法:
image-20201213090302933

模式的使用

现代软件设计的特征是需求的频繁变化。设计模式的要点是寻找变化点,然后在变化点处应用设计模式,从而来更好地应对需求的变化。什么时候、什么地点应用设计模式比理解设计模式结构本身更为重要。

设计模式的宗旨是管理变化,提高复用性。设计模式不是把变化消灭了,而是把变化点和稳定点解耦合,然后将变化点转移集中到某一个或几个类中,来更好地应对变化。例如通过 Java 的反射机制配合配置文件可以方便地替换加载不同的类。

假设一种极端情况,软件中的所有都在变化,那么没有一种设计模式能解决问题。反之如果软件所有部分都是稳定的,没有变坏点,那么也没必要使用设计模式。绝大多数软件则是既有稳定的一部分,也有变化的一部分。通常 Framework 的开发人员对设计模式的理解要求更高,因为 Framework 往往要给 Application 开发者预留下很多接口和扩展点应应对将来可能的变化。

不要拘泥于 GOF 提出的 23 种设计模式,代码只要符合设计原则,解决耦合性问题,提高了复用性,就可以把它当做是一种模式的应用。

在什么时候不应该使用设计模式:

  • 代码可读性很差时,先提高代码可读性。
  • 需求理解还很浅时,先搭建一个快速原型版本了解用户需求,再在后续的迭代中使用设计模式。
  • 变化没有显现时,不要过度使用设计模式去预测变化。
  • 不是系统过的关键依赖点,优先在关键依赖点使用模式。
  • 项目没有复用价值时,典型的就是一些外包软件,甲方没有后期的软件升级迭代计划,就没有去使用设计模式的价值。反之,如果是自己公司的软件产品,前期使用好的设计模式能够大大减少后期需求变更时的工作量。
  • 项目将要发布时,使用模式重构可能给项目带来新的 Bug。

重构到模式

设计模式的应用不宜先入为主,一上来就使用设计模式是对设计模式的最大误用。没有一步到位的设计模式。敏捷软件开发实践提倡的“Refactoring to Patterns”是目前普遍公认的最好的使用设计模式的方法。

重构获得模式(Refactoring to Patterns):审视现有代码违反了哪些设计原则,在重构软件的过程中改善现有设计,一步步迭代到设计模式。良好的设计模式是演化的结果,不要追求一步到位,除非你是一个设计模式大师。

可以从下面 5 个重构的关键技法考虑优化代码的可能性:

  • 静态 → 动态
  • 早绑定 → 晚绑定
  • 继承 → 组合
  • 编译时依赖 → 运行时依赖
  • 紧耦合 → 松耦合

组件协作模式

现代软件专业分工之后的第一个结果是“框架与应用程序的划分”,“组件协作”模式通过晚期绑定,来实现框架与应用程序之间的松耦合,是二者之间协作时常用的模式。通过组件协作模式,我们可以调用这些第三方库或框架,补充上特定的上层应用业务代码,就可以快速搭建出产品,而不用去造底层的轮子。

模板方法 Template Method

模板方法模式:定义一个操作中算法的框架,而将一些步骤延迟到子类中。模板方法模式使得子类不改变一个算法的结构即可重定义该算法的某些特定步骤。

Template Method Pattern: Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.——《Design Patterns: Elements of Reusable Object-Oriented Software》

某个算法框架由很多基本方法 (Primitive Method)组成,在抽象的父类中提供一个称之为模板方法(Template Method)的方法来定义这些基本方法的执行次序,而通过其子类来覆盖重写(override)某些步骤,从而使得相同的算法框架可以有不同的执行结果。

被模板调用的基本方法可以是具体方法(Concrete Method),空实现的钩子方法(Hook Method,钩子方法通常返回一个 boolean 类型的值,并以此来判断是否执行某一基本方法,因此在子类中可以通过覆盖钩子方法来决定是否执行父类中的某一方法,从而实现子类对父类行为的控制。),也可以是没有任何实现的抽象方法(Abstract Method),一般推荐将它们设置为 protected 方法,因为这些方法通常只会让子类来重写才有意义,而不直接提供对外访问。

动机:在软件构建过程中,对于某一项任务,它常常有稳定的整体操作结构,但各个子步骤却有很多改变的需求,或者由于固有的原因(比如框架与应用之间的关系)而无法和任务的整体结构同时实现。

应用实例:“不要调用我,让我来调用你”的反向控制结构(Inverse of Control:Don’t call me, Let me call you.)是 Template Method 的典型应用。在早期应用程序开发者往往会写一个程序主流程来调用早已经写好的库和框架中的方法(早绑定)。而现在很多框架则是根据写好的模板方法的流程来调用应用程序开发者对基本方法的实现或者覆写(晚绑定)。这样应用程序开发者将程序的流程控制权还给框架,让框架来调用自己写的应用程序中的方法,而不是自己写程序去主动调用框架中的方法。但这样可能会让应用程序开发者只见树木,不见森林,看不到框架中调用方法的步骤和细节,所以必要时还是需要阅读框架源码查看底层细节。

结构类图:

image-20201213094249455

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public abstract class AbstractClass {
//模板方法
public void templateMethod() {
primitiveOperation1();
primitiveOperation2();
primitiveOperation3();
}

//基本方法—具体方法
public void primitiveOperation1() {
//实现代码
}

//基本方法—抽象方法
public abstract void primitiveOperation2();

//基本方法—钩子方法
public void primitiveOperation3()
{ }
}

策略模式 Strategy

策略模式:定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法可以独立于使用它的客户变化。

Strategy Pattern: Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.——《Design Patterns: Elements of Reusable Object-Oriented Software》

策略模式又被称为政策(Policy)模式,每一个封装算法的类称之为策略(Strategy)类,策略模式提供了一种可插入式(Pluggable)算法的实现方案,同过传入不同的实现策略接口的对象,为组件提供了一系列可重用的算法,从而可以使得类型在运行时方便地根据需要在各个算法之间进行切换。通常上下文可以共享同一个 Strategy 对象,结合单例模式可以节省对象开销。

动机:在软件构建过程中,某些对象使用的算法可能多种多样,经常改变,如果将这些算法都硬编码(Hard Coding)到对象中,将会使对象变得异常复杂;而且有时候支持不使用的算法也是一个性能负担(因为要白白执行很多判断语句)。

应用实例:当实现某个目标的算法不止一条,例如排序、查找、折扣计算等,使用策略模式可以在运行时方便地根据需要在各个算法之间进行切换。当含有大量嵌套的像 if-else 或者 switch-case 条件判断语句的代码通常往往都要考虑都需 Strategy 模式来消除条件判断语句解耦合。但是如果是性别和星期这种绝对稳定不变,没有变化可能的就不需要使用策略模式。

结构类图:

image-20201214220657032

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//抽象策略类
public abstract class Strategy {
public abstract void algorithm(); //声明抽象算法
}
//具体策略类A
public class ConcreteStrategyA extends Strategy {
//算法的具体实现
public void algorithm() {
//算法A
}
}
//具体策略类A
public class ConcreteStrategyB extends Strategy {
//算法的具体实现
public void algorithm() {
//算法B
}
}
//上下文环境类
public class Context {
private Strategy strategy; //维持一个对抽象策略类的引用

//注入策略对象
public void setStrategy(Strategy strategy) {
this.strategy= strategy;
}

//调用策略类中的算法
public void algorithm() {
strategy.algorithm();
}
}

观察者模式 Observer

观察者模式:定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象都得到通知并被自动更新。

Observer Pattern: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.——《Design Patterns: Elements of Reusable Object-Oriented Software》

观察者模式又被称为发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式、从属者(Dependents)模式。

定义了对象之间一种一对多的依赖关系,让一个对象的改变能够影响其他对象。发生改变的对象称为观察目标,被通知的对象称为观察者。一个观察目标可以对应多个观察者,观察目标发送通知时,无需指定观察者,通知会自动传播。观察者自己决定是否需要订阅通知,目标对象对此一无所知。

动机:在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系” ——一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化。

应用实例:

  • 网上商店中商品在名称、价格等发生变化,系统自动通知收藏该商品的会员。
  • 基于事件的 UI 框架。
  • 在传统的 MVC(Model-View-Controller)模式中,模型可对应于观察者模式中的观察目标,而视图对应于观察者,控制器可充当两者之间的中介者,当模型层的数据发生改变时,视图层将自动改变其显示内容。
  • JDK 中 java.util.Observerjava.util.Observable 这两个类提供 Java 对观察者模式的支持:

结构类图:

image-20201214223247310

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//抽象目标类
public abstract class Subject {
//定义一个观察者集合用于存储所有观察者对象
protected ArrayList observers<Observer> = new ArrayList();

//注册方法,用于向观察者集合中增加一个观察者
public void attach(Observer observer) {
observers.add(observer);
}

//注销方法,用于在观察者集合中删除一个观察者
public void detach(Observer observer) {
observers.remove(observer);
}

//声明抽象通知方法
public abstract void notify();
}
//具体目标类
public class ConcreteSubject extends Subject {
//实现通知方法
public void notify() {
//遍历观察者集合,调用每一个观察者的响应方法
for(Object obs:observers) {
((Observer)obs).update();
}
}
}

单一职责模式

在软件组件的设计中,如果责任划分的不清晰,使用继承得到的结果往往是随着需求的变化,子类急剧膨胀,同时充斥着重复代码,这时候的关键是划清责任。

装饰模式 Decorator

装饰模式:动态地给一个对象增加一些额外的职责。就扩展功能而言,装饰模式提供了一种比使用子类更加灵活的替代方案。

Decorator Pattern: Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.——《Design Patterns: Elements of Reusable Object-Oriented Software》

装饰模式通过一种无须定义子类的方式给对象动态增加职责,采用组合而非继承的手法,在不改变一个对象本身功能的基础上给对象增加额外的新行为,是一种用于替代继承的技术。Decorator 模式实现了在运行时
动态扩展对象功能的能力,而且可以根据需要扩展多个功能。在装饰类中既可以调用待装饰的原有类的方法,还可以增加新的方法,以扩展原有类的功能。Decorator 类在接口上表现为 is-a Component 的继承关系,即 Decorator 类继承了 Component 类所具有的接口。但在实现上又表现为 has-a Component 的组合关系,即 Decorator 类又关联了一个 Component 对象。

透明(Transparent)装饰模式:要求客户端完全针对抽象编程,不将对象的引用声明为具体构件类型或具体装饰类型,而应该全部声明为抽象构件类型。对于客户端而言,具体构件对象和具体装饰对象没有任何区别。可以让客户端透明地使用装饰之前的对象和装饰之后的对象,无须关心它们的区别,可以对一个已装饰过的对象进行多次装饰,得到更为复杂、功能更为强大的对象,但是无法在客户端单独调用装饰类里面的新增方法。

半透明(Semi-transparent)装饰模式:用具体装饰类型来声明装饰之后的对象,而具体构件使用抽象构件类型来声明。对于客户端而言,具体构件类型无须关心,是透明的,但是具体装饰类型必须指定,这是不透明的。可以单独调用装饰类里面的新增方法,但是不能如果对用一个对象的多次装饰,只能调用到最后一次装饰时具体装饰类中新增加的方法,无法调用到之前装饰时新增的方法。

动机:在某些情况下我们可能会“过度地使用继承来扩展对象的功能”,由于继承为类型引入的静态特质,使得这种扩展方式缺乏灵活性;并且随着子类的增多(扩展功能的增多),各种子类的组合(扩展功能的组合)会导致更多子类的膨胀。

应用实例:当一个类既继承某一个类又聚合了这个类的对象,并且存在一个以这个类的对象为参数的构造方法,往往就使用到了装饰模式。Java 的 I/O 中使用装饰模式来为输入和输出字符流添加期望的功能。嵌套地调用构造方法也是使用到装饰模式的显著特征:BufferedReader br = new BufferedReader(new InputStreamReader(System.in)).。

image-20201215110448194

结构类图:

image-20201214225133922

代码:

1
2
3
4
5
6
7
8
9
10
11
12
public class Decorator extends Component {
private Component component; //维持一个对抽象构件对象的引用

//注入一个抽象构件类型的对象
public Decorator(Component component) {
this.component=component;
}

public void operation() {
component.operation(); //调用原有业务方法
}
}

桥接模式 Bridge

桥接模式:将抽象部分与它的实现部分解耦,使得两者都能够独立变化。

Bridge Pattern: Decouple an abstraction from its implementation so that the two can vary independently.——《Design Patterns: Elements of Reusable Object-Oriented Software》

桥接模式又被称为柄体(Handle and Body)模式或接口(Interface)模式。Bridge 模式使用“对象间的组合关系”解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。所谓抽象和实现沿着各自纬度的变化,即“子类化”它们。Bridge 模式有时候类似于多继承方案,但是多继承方案往往违背单一职责原则(即一个类只有一个变化的原因),复用性比较差。Bridge 模式是比多继承方案更好的解决方法。

动机:由于某些类型的固有的实现逻辑,使得它们具有两个变化的维度,乃至多个维度的变化。

应用实例:Bridge 模式的应用一般在“两个非常强的变化维度”,例如一款毛笔在型号和颜色有两个变化维度,使用桥接模式让颜色和型号实现了分离,增加新的颜色或者型号对另一方没有任何影响。

image-20201215111848313

结构类图:

image-20201215111239464

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//实现类接口
public interface Implementor {
public void operationImpl();
}
//具体实现类
public class ConcreteImplementor implements Implementor {
public void operationImpl() {
//具体业务方法的实现
}
}
//抽象类
public abstract class Abstraction {
protected Implementor impl; //定义实现类接口对象

public void setImpl(Implementor impl) {
this.impl=impl;
}

public abstract void operation(); //声明抽象业务方法
}
//扩充抽象类(细化抽象类)
public class RefinedAbstraction extends Abstraction {
public void operation() {
//业务代码
impl.operationImpl(); //调用实现类的方法
//业务代码
}
}

对象创建模式

通过“对象创建” 模式绕开 new,来避免对象创建(new)过程中所导致的紧耦合(依赖具体类),从而支持对象创建的稳定。它是接口抽象之后的第一步工作。

简单工厂模式 Simple Factory

简单工厂模式(Simple Factory Pattern):定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。

简单工厂模式又被称为静态工厂方法(Static Factory Method)模式,因为在简单工厂模式中用于创建实例的方法通常是静态(static)方法。对象创建和使用的分离,客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可。简单工厂模式并不在 GOF 所提出的 23 种设计模式之列。

动机:客户端只知道传入工厂类的参数,对于如何创建对象并不关心。工厂类负责创建的对象比较少,由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。

应用实例:下图的图表工厂可以通过传入不同的类型参数即可得到不同类型的图表。

image-20201215114345630

结构类图:

image-20201215112908212

简单工厂的简化:将抽象产品类和工厂类合并,将静态工厂方法移至抽象产品类中

image-20201215113630774

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Factory {
//静态工厂方法
public static Product getProduct(String arg) {
Product product = null;
if (arg.equalsIgnoreCase("A")) {
product = new ConcreteProductA();
//初始化设置product
}
else if (arg.equalsIgnoreCase("B")) {
product = new ConcreteProductB();
//初始化设置product
}
return product;
}
}

工厂方法 Factory Method

工厂方法模式:定义一个用于创建对象的接口,但是让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类。

Factory Method Pattern: Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.——《Design Patterns: Elements of Reusable Object-Oriented Software》

工厂模式(Factory Pattern)又被称为虚拟构造器模式(Virtual Constructor Pattern)或多态工厂模式(Polymorphic Factory Pattern),工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象,目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。

动机:在软件系统中,经常面临着创建对象的工作;由于需求的变化,需要创建的对象的具体类型经常变化。

应用举例:日志记录器(Logger)工厂可以封装记录器的初始化过程并保证多种记录器切换的灵活性。

image-20201215115120118

结构类图:

image-20201215114920393

代码:

1
2
3
4
5
6
7
8
public interface Factory {
public Product factoryMethod();
}
public class ConcreteFactory implements Factory {
public Product factoryMethod() {
return new ConcreteProduct();
}
}

抽象工厂 Abstract Factory

抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。

Abstract Factory Pattern: Provide an interface for creating families of related or dependent objects without specifying their concrete classes.——《Design Patterns: Elements of Reusable Object-Oriented Software》

抽象工厂模式又称为工具(Kit)模式,抽象工厂模式中的具体工厂不只是创建一种产品,它负责创建一族产品,也可以叫它家族工厂,产品族工厂。抽象工厂可以很方便的增加新的产品族,无须修改已有系统,只需要增加具体产品并对应增加一个新的具体工厂。但若是要增加新的产品等级结构,则需要修改所有的工厂角色,包括抽象工厂类,在所有的工厂类中都需要增加生产新产品的方法。抽象工厂模式是所有形式的工厂模式中最为抽象和最具一般性的一种形式。如果没有多个产品族对象创建的需求变化,完全可以使用简单工厂模式,如果一个产品族只有一种产品,则完全可以使用抽象工厂模式。

动机:在软件系统中,经常面临着“一系列相互依赖的对象”的创建工作;同时,由于需求的变化,往往存在更多系列对象的创建工作。

应用实例:海尔工厂生产海尔产品族中的所有产品。

image-20201215120824157

结构类图:

image-20201215120210674

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//抽象工厂类
public interface AbstractFactory {
public AbstractProductA createProductA(); //工厂方法一
public AbstractProductB createProductB(); //工厂方法二
……
}
//具体工厂类
public class ConcreteFactory1 implements AbstractFactory {
//工厂方法一
public AbstractProductA createProductA() {
return new ConcreteProductA1();
}

//工厂方法二
public AbstractProductB createProductB() {
return new ConcreteProductB1();
}
……
}

原型模式 Prototype

原型模式:使用原型实例指定待创建对象的类型,并且通过复制这个原型来创建新的对象。

Prototype Pattern: Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.——《Design Patterns: Elements of Reusable Object-Oriented Software》

原型方法通过复制一个原型对象得到与原型对象一模一样的新对象,相比使用工厂方法,原型方法把创建任务回归到类本身中来,创建新对象(也称为克隆对象)的工厂就是原型类自身,原型对象通过复制自己来实现创建过程。原型模式常用来创建结构复杂的对象,使用工厂方还需要对创建的对象进行大量初始化操作才能得到自己想要的状态,使用原型模式可以直接从一个理想的中间状态的原型对象克隆出新对象,再根据需要对其成员变量稍作修改。

浅克隆与深克隆:

  • 浅克隆(Shallow Clone):当原型对象被复制时,只复制它本身和其中包含的值类型的成员变量,而引用类型的成员变量并没有复制
    image-20201215130409732
  • 深克隆(Deep Clone):除了对象本身被复制外,对象所包含的所有成员变量也将被复制
    image-20201215130353383

动机:在软件系统中,经常面临着“某些结构复杂的对象”的创建工作;由于需求的变化,这些对象经常面临着剧烈的变化,但是它们却拥有比较稳定一致的接口。

应用实例:Java 中 Object 类已经提供了浅克隆的实现,可以根据自己需要来在类中实现 Cloneable 接口来重写 clone() 方法,手动给成员变量赋值或者利用序列化来实现深克隆,clone() 方法并不是 Cloneable 接口中的方法,而是 Object 类中的方法,Cloneable 只是一个空的标记接口。

结构类图:

image-20201215122228277

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ConcretePrototype implements Cloneable {
//浅克隆
public Object clone() {
Object object = null;
try {
object = super.clone();
}
catch(CloneNotSupportedExecption exception) {
System.err.println("Not support cloneable");
}
return (ConcretePrototype)object;
}
}

构建器 Builder

建造者模式:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

Builder Pattern: Separate the construction of a complex object from its representation so that the same construction process can create different representations.——《Design Patterns: Elements of Reusable Object-Oriented Software》

建造者模式可以将部件本身和它们的组装过程分开,关注如何一步步创建一个包含多个组成部分的复杂对象,用户只需要指定复杂对象的类型即可得到该对象,而无须知道其内部的具体构造细节。Builder 模式主要用于“分步骤构建一个复杂的对象”。在这其中“分步骤”是一个稳定的算法,而复杂对象的各个部分则经常变化。

动机:在软件系统中,有时候面临着“一个复杂对象”的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。

应用实例:使用建造者模式来实现游戏人物模型的创建,根据不同的角色先逐步创建其组成部分,再将各组成部分装配成一个完整的游戏角色。

image-20201215171434220

结构类图:

image-20201215161637683

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//复杂对象类
public class Product {
private String partA; //定义部件,部件可以是任意类型,包括值类型和引用类型
private String partB;
private String partC;

//partA的Getter/Setter方法省略
//partB的Getter/Setter方法省略
//partC的Getter/Setter方法省略
}
//抽象建造者类
public abstract class Builder {
//创建产品对象
protected Product product=new Product();
public abstract void buildPartA();
public abstract void buildPartB();
public abstract void buildPartC();

//返回产品对象
public Product getResult() {
return product;
}
}
//具体建造者类
public class ConcreteBuilder1 extends Builder{
public void buildPartA() {
product.setPartA("A1");
}

public void buildPartB() {
product.setPartB("B1");
}

public void buildPartC() {
product.setPartC("C1");
}
}
//指挥者类
public class Director {
private Builder builder;

public Director(Builder builder) {
this.builder=builder;
}

public void setBuilder(Builder builder) {
this.builder=builer;
}

//产品构建与组装方法
public Product construct() {
builder.buildPartA();
builder.buildPartB();
builder.buildPartC();
return builder.getResult();
}
}

对象性能模式

面向对象很好地解决了“抽象”的问题,但是必不可免地要付出—定的代价。对于通常情况来讲,面向对象的成本大都可以忽略不计。但是某些情况,面向对象所带来的成本必须谨慎处理。

单例模式 Singleton

单例模式:确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。

Singleton Pattern: Ensure a class has only one instance, and provide a global point of access to it.——《Design Patterns: Elements of Reusable Object-Oriented Software》

让类自身负责创建和保存它的唯一实例,保证不能创建其他实例,并提供一个访问该实例的方法。和工厂不同,单例模式之所以不直接用 new 来创建创建,绕过常规的构造器,不是为了解耦合,而是为了逻辑正确、安全和性能(节省对象开销)。

动机:在软件系统中,经常有这样一些特殊的类,必须保证它们在系统中只存在一个实例,才能确保它们的逻辑正确性、以及良好的效率。

应用实例:Windows 操作系统中的任务管理器和回收站、Java 的 Runtime 类实例、各种连接池。

结构类图:

image-20201215173916775

代码:

  1. 饿汉式单例模式(EagerSingleton):类加载到内存就实例化一个单例,JVM会保证线程安全,无须考虑多个线程同时访问的问题,调用速度和反应时间优于懒汉式单例。不管用到与否,只要加载了这个类就会实例化,,系统加载时间可能会比较长,资源利用效率不及懒汉式单例。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class EagerSingleton { 
    //声明实例为 static 类变量
    private static final EagerSingleton instance = new EagerSingleton();
    //构造器 private 防止被其他类调用
    private EagerSingleton() { }
     
    public static EagerSingleton getInstance() {
    return instance;
    }
    }
  2. 懒汉式单例类(LazySingleton):实现了延迟加载,解决了饿汉式的缺点,但是带来了线程不安全问题,可能导致有多个实例被创建,需通过双重检查锁定等机制进行控制,处理多个线程同时访问的问题,这将导致系统性能受到一定影响。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    //方法锁,在获取实例方法上加锁,有较高的性能损耗,
    public class LazySingleton {
    private static LazySingleton instance = null;

    private LazySingleton() { }
    //给获取实例加锁
    synchronized public static LazySingleton getInstance() {
    if (instance == null) {
    instance = new LazySingleton();
    }
    return instance;
    }
    }
    //锁代码块,在局部代码块加锁,并双重检查锁定(Double-Check Locking)
    public class LazySingleton {
    //由于编译器指令重排(reorder)优化导致多线程不安全,可能在一个线程拿到锁但还没有来得及执行构造器,另一线程就提前得到了返回地址使用静态内部类,在静态内部类里面声明一个static修饰的外部类变量
    private volatile static LazySingleton instance = null;

    private LazySingleton() { }

    public static LazySingleton getInstance() {
    //第一重判断
    if (instance == null) {
    //锁定代码块
    synchronized (LazySingleton.class) {
    //第二重判断,instance 要用 volatile 修饰
    if (instance == null) {
    instance = new LazySingleton(); //创建单例实例
    }
    }
    }
    return instance;
    }
    }
  3. 使用静态内部类,在静态内部类里面声明一个 static 修饰的外部类变量,既实现了懒加载,又保证了线程安全,使用静态内部类需要语言特性支持。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //Initialization on Demand Holder
    public class Singleton {
    private Singleton() {
    }

    //静态内部类
    private static class HolderClass {
    private final static Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
    return HolderClass.instance;
    }
    }

  4. 使用枚举类产生单例,不仅可以解决线程同步,还可以防止利用反序列化和反射来产生单例,这是《Effective Java》中所推荐的方法。

    1
    2
    3
    4
    5
    //使用 Singleton.INSTANCE 来引用单例对象
    public enum Singleton {
    INSTANCE;
    ……
    }

享元模式 Flyweight

享元模式:运用共享技术有效地支持大量细粒度对象的复用。

Flyweight Pattern: Use sharing to support large numbers of fine-grained objects efficiently.——《Design Patterns: Elements of Reusable Object-Oriented Software》

将具有相同内部状态的对象存储在享元池(Flyweight Pool)中,享元池中的对象是可以实现共享的,需要的时候将对象从享元池中取出,即可实现对象的复用,减少内存中对象的数量。通过向取出的对象注入不同的外部状态,可以得到一系列相似的对象,使得相同或者相似的对象在内存中只保存一份,从而可以节约系统资源,提高系统性能。享元的共享状态值往往是只读的。

动机:在软件系统采用纯粹面向对象的问题在于大量细粒度的对象会很快充斥在系统中,从而带来很高的运行时代价——主要指内存需求方面的代价。

应用举例:Java 中 String 字符串类。

结构类图:

image-20201215182815456

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//抽象享元类
public abstract class Flyweight {
public abstract void operation(String extrinsicState);
}
//具体享元类
public class ConcreteFlyweight extends Flyweight {
//内部状态intrinsicState作为成员变量,同一个享元对象其内部状态是一致的
private String intrinsicState;
public ConcreteFlyweight(String intrinsicState) {
this.intrinsicState = intrinsicState;
}

//外部状态extrinsicState在使用时由外部设置,不保存在享元对象中,即使是同一个对象,在每一次调用时可以传入不同的外部状态
public void operation(String extrinsicState) {
//实现业务方法
}
}
//非共享具体享元类
public class UnsharedConcreteFlyweight extends Flyweight {
public void operation(String extrinsicState) {
//实现业务方法
}
}
//享元工厂类
public class FlyweightFactory {
//定义一个HashMap用于存储享元对象,实现享元池
private HashMap flyweights = new HashMap();

public Flyweight getFlyweight(String key) {
//如果对象存在,则直接从享元池获取
if (flyweights.containsKey(key)) {
return (Flyweight)flyweights.get(key);
}
//如果对象不存在,先创建一个新的对象添加到享元池中,然后返回
else {
Flyweight fw = new ConcreteFlyweight();
flyweights.put(key,fw);
return fw;
}
}
}

接口隔离模式

在组件构建过程中,某些接口之间直接的依赖常常会带来很多问题、甚至根本无法实现。采用添加一层间接(稳定)接口,来隔离本来互相紧密关联的接口是一种常见的解决方案。

外观模式 Facade

外观模式:为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

Facade Pattern: Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.——《Design Patterns: Elements of Reusable Object-Oriented Software》

外观模式又称为门面模式,引入一个新的外观类(Facade)来负责和多个子系统(Subsystem)进行交互,而客户类只需与外观类交互,为多个业务类的调用提供了一个统一的入口,客户类与子系统之间原有的复杂引用关系由外观类来实现,简化了类与类之间的交互,从而降低了系统的耦合度。子系统是一个广义的概念,它可以是一个类、一个功能模块、系统的一个组成部分或者一个完整的系统。外观类往往只需要一个实例,可以结合单例模式。

动机:组件的客户和组件中各种复杂的子系统有了过多的耦合,随着外部客户程序和各子系统的演化,这种过多的耦合面临很多变化的挑战。

应用实例:大型公司的前台是企业内部与外界交互的接口,访客通过前台的窗口来和各个部门打交道,而不是直接进入公司内部的各个部门。

结构类图:

image-20201215184445383

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//各个子系统
public class SubSystemA {
public void methodA() {
//业务实现代码
}
}
public class SubSystemB {
public void methodB() {
//业务实现代码
}
}
public class SubSystemC {
public void methodC() {
//业务实现代码
}
}
//外观类
public class Facade {
private SubSystemA obj1 = new SubSystemA();
private SubSystemB obj2 = new SubSystemB();
private SubSystemC obj3 = new SubSystemC();

public void method() {
obj1.method();
obj2.method();
obj3.method();
}
}

代理模式 Proxy

代理模式:给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。

Proxy Pattern: Provide a surrogate or placeholder for another object to control access to it.——《Design Patterns: Elements of Reusable Object-Oriented Software》

代理模式又称为经纪人(Surrogate/Broker)模式,两个对象本来可以直接依赖,但是由于性能、安全或者分布式的原因必须隔离。通过引入一个新的代理对象,代理对象在客户端对象和目标对象之间起到中介的作用,代理对象中去掉客户不能看到的内容和服务或者增添客户需要的额外的新服务。

几种常见的代理模式:

  • 远程代理(Remote Proxy):为一个位于不同的地址空间的对象提供一个本地的代理对象,这个不同的地址空间可以在同一台主机中,也可以在另一台主机中,远程代理又称为大使(Ambassador)。
  • 虚拟代理(Virtual Proxy):如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。
  • 保护代理(Protect Proxy):控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。
  • 缓冲代理(Cache Proxy):为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。
  • 智能引用代理(Smart Reference Proxy):当一个对象被引用时,提供一些额外的操作,例如将对象被调用的次数记录下来等。
  • 动态代理(Dynamic Proxy):可以让系统在运行时根据实际需要来动态创建代理类,让同一个代理类能够代理多个不同的真实主题类而且可以代理不同的方法。

动机:在面向对象系统中,有些对象由于某种原因(比如对象创建的开销很大,或者某些操作需要安全控制,或者需要进程外的访问等),直接访问会给使用者、或者系统结构带来很多麻烦。

应用实例:Java RMI 远程代理框架就用到了代理模式。RMI(Remote Method Invocation)是基于 Java 技术的分布式编程模型,为 Java 程序提供远程访问服务。通过 RMI 允许对象在不同的 Java 虚拟机(Java Virtual Machine)之间进行通信。此外 JDK 中的 java.lang.reflect 这个包还提供对了动态代理的支持。

结构类图:

image-20201215185123943

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//抽象主题类
public abstract class Subject {
public abstract void request();
}
//真实主题类
public class RealSubject extends Subject{
public void request() {
//业务方法具体实现代码
}
}
//代理类
public class Proxy extends Subject {
private RealSubject realSubject = new RealSubject(); //维持一个对真实主题对象的引用 
public void preRequest() {
…...
}
 
public void request() {
preRequest();
realSubject.request(); //调用真实主题对象的方法
postRequest();
}
 
public void postRequest() {
……
}
}

适配器模式 Adapter

适配器模式:将一个类的接口转换成客户希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作。

Adapter Pattern: Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.——《Design Patterns: Elements of Reusable Object-Oriented Software》

适配器模式又名包装器(Wrapper)模式,适配器模式解决的是老旧接口不兼容需要转换的问题。通过引入一个适配器类来重用现有的适配者类,将目标类和适配者类解耦,无须修改原有结构,增加了类的透明性和复用性,提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用。

类适配器和对象适配器:

  • 类适配器模式置换一些适配者的方法很方便,但是 Java 中一次最多只能适配一个适配者类,并且目标抽象类只能为接口,因为 Java 不支持多继承。
  • 对象适配器模式采用对象组合的方式,可以把多个不同的适配者适配到同一个目标,更加推荐使用。

动机:在软件系统中,由于应用环境的变化,常常需要将“一些现存的对象”放在新的环境中应用,但是新环境要求的接口是这些现存对象所不满足的。

应用实例:给玩具汽车添加上警灯闪烁和警笛音效。

image-20201215200322719

结构类图:

类适配器:

image-20201215193234377

对象适配器:

image-20201215193244032

双向适配器:

image-20201215193406010

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//类适配器
public class Adapter extends Adaptee implements Target {
public void request() {
super.specificRequest();
}
}
//对象适配器
public class Adapter extends Target {
private Adaptee adaptee; //维持一个对适配者对象的引用

public Adapter(Adaptee adaptee) {
this.adaptee=adaptee;
}

public void request() {
adaptee.specificRequest(); //转发调用
}
}
//双向适配器
public class Adapter implements Target,Adaptee {
private Target target;
private Adaptee adaptee;

public Adapter(Target target) {
this.target = target;
}

public Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}

public void request() {
adaptee.specificRequest();
}

public void specificRequest() {
target.request();
}
}

中介者模式 Mediator

中介者模式:定义一个对象来封装一系列对象的交互。中介者模式使各对象之间不需要显式地相互引用,从而使其耦合松散,而且让你可以独立地改变它们之间的交互。

Mediator Pattern: Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.——《Design Patterns: Elements of Reusable Object-Oriented Software》

中介者模式又称为调停者模式,在中介者模式中,通过引入中介者来简化对象之间的复杂交互,中介者模式是迪米特法则的一个典型应用,对象之间多对多的复杂关系转化为相对简单的一对多关系。中介者模式将系统的网状结构变成以中介者为中心的星型结构,同事对象不再直接与另一个对象联系,它通过中介者对象与另一个对象发生相互作用。新对象的引入不会给系统的结构带来大量的修改工作。中介者模式解决的是系统内多个对象之间的通信问题,减少类之间的关联。外观模式解决的是客户类和子系统类的通信问题。中介者模式中对象可以向中介者请求协作。外观模式中子系统对象不会对外观有任何协作请求。

动机:在软件构建过程中,经常会出现多个对象互相关联交互的情况对象之间常常会维持一种复杂的引用关系,如果遇到一些需求的更改,这种直接的引用关系将面临不断的变化。

应用实例:房屋中介公司扮演了出租者、求租者的中介者。

结构类图:

image-20201215200807616

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//抽象中介者类
public abstract class Mediator {
protected ArrayList<Colleague> colleagues = new ArrayList<Colleague>(); //用于存储同事对象

//注册方法,用于增加同事对象
public void register(Colleague colleague) {
colleagues.add(colleague);
}

//声明抽象的业务方法
public abstract void operation();
}
//具体中介者类
public class ConcreteMediator extends Mediator {
//实现业务方法,封装同事之间的调用
public void operation() {
......
((Colleague)(colleagues.get(0))).method1(); //通过中介者调用同事类的方法
......
}
}
//抽象同事类
public abstract class Colleague {
protected Mediator mediator; //维持一个抽象中介者的引用

public Colleague(Mediator mediator) {
this.mediator=mediator;
}

public abstract void method1(); //声明自身方法,处理自己的行为

//定义依赖方法,与中介者进行通信
public void method2() {
mediator.operation();
}
}
//具体同事类
public class ConcreteColleague extends Colleague {
public ConcreteColleague(Mediator mediator) {
super(mediator);
}

//实现自身方法
public void method1() {
......
}
}

状态变化模式

在组件构建过程中,某些对象的状态经常面临变化,如何对这些变化进行有效的管理?同时又维持高层模块的稳定?“状态变化”模式为这一问题提供了一种解决方案。

状态模式 State

状态模式:允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。

State Pattern: Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.——《Design Patterns: Elements of Reusable Object-Oriented Software》

状态模式将一个对象的状态从该对象中分离出来,封装到专门的状态类中,使得对象状态可以灵活变化,对于客户端而言,无须关心对象状态的转换以及对象所处的当前状态,无论对于何种状态的对象,客户端都可以一致处理。用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题。通常上下文可以共享一个状态实例,结合单例模式可以节省对象开销。

动机:在软件构建过程中,某些对象的状态如果改变,其行为也会随之而发生变化,比如文档处于只读状态,其支持的行为和读写状态支持的行为就可能完全不同。

应用实例:银行根据账户的余额实现账户的状态自动装换。

image-20201215202655621

结构类图:

image-20201215202313003

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//抽象状态类
public abstract class State {
//声明抽象业务方法,不同的具体状态类可以有不同的实现
public abstract void handle();
}
//具体状态类
public class ConcreteState extends State {
public void handle() {
//方法具体实现代码
}
}
//环境上下文类
public class Context {
private State state; //维持一个对抽象状态对象的引用
private int value; //其他属性值

public void setState(State state) {
this.state = state;
}

public void request() {
//其他代码
state.handle(); //调用状态对象的业务方法
//其他代码
}
}

备忘录 Memento

备忘录模式:在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样就可以在以后将对象恢复到原先保存的状态。

Memento Pattern: Without violating encapsulation, capture and externalize an object’s internal state so that the object can be restored to this state later.——《Design Patterns: Elements of Reusable Object-Oriented Software》

备忘录模式又名标记(Token)模式,提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤。首先保存软件系统的历史状态,当用户需要取消错误操作并且返回到某个历史状态时,可以取出事先保存的历史状态来覆盖当前状态。现在很少使用备忘录模式,而是采用效率更高更易实现的序列化方式来实现快照。

动机:在软件构建过程中,某些对象的状态在转换过程中,可能由于某种需要,要求程序能够回溯到对象之前处于某个点时的状态。如果使用一些公有接口来让其他对象得到对象的状态,便会暴露对象的细节实现。

应用实例:很多软件所提供的撤销(Undo)和重做(Redo)操作中就使用了备忘录模式。

结构类图:

image-20201215203410088

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//原发器类
public class Originator {
private String state;

public Originator(){}

//创建一个备忘录对象
public Memento createMemento() {
return new Memento(this);
}

//根据备忘录对象恢复原发器状态
public void restoreMemento(Memento m) {
state = m.state;
}

public void setState(String state) {
this.state=state;
}

public String getState() {
return this.state;
}
}
//将Memento类与Originator类定义在同一个包(package)中来实现封装,使用默认可见性定义Memento类,即保证其在包内可见。或者将备忘录类作为原发器类的内部类,使得只有原发器才可以访问备忘录中的数据,其他对象都无法使用备忘录中的数据。
//备忘录类,默认可见性,包内可见
class Memento {
private String state;

Memento(Originator o) {
state = o.getState();
}

void setState(String state) {
this.state=state;
}

String getState() {
return this.state;
}
}
//负责人类
public class Caretaker {
private Memento memento;

public Memento getMemento() {
return memento;
}

public void setMemento(Memento memento) {
this.memento=memento;
}
}

数据结构模式

常常有一些组件在内部具有特定的数据结构,如果让客户程序依赖这些特定的数据结构,将极大地破坏组件的复用。这时候,将这些特定数据结构封装在内部,在外部提供统一的接口,来实现与特定数据结构无关的访问,是一种行之有效的解决方案。

组合模式 Composite

组合模式:组合多个对象形成树形结构以表示具有部分-整体关系的层次结构。组合模式让客户端可以统一对待单个对象和组合对象。

Composite Pattern: Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.——《Design Patterns: Elements of Reusable Object-Oriented Software》

组合模式又称为“部分-整体”(Part-Whole)模式。组合模式通过一种巧妙的设计方案使得用户可以一致性地处理整个树形结构或者树形结构的一部分,它描述了如何将容器对象和叶子对象进行递归组合,使得用户在使用时无须对它们进行区分,可以一致地对待容器对象和叶子对象,无需关心处理的是叶子对象还是容器对象。

透明组合模式和安全组合模式:

  • 透明组合模式:抽象构件 Component 中声明了所有用于管理成员对象的方法,包括 add()remove(),以及 getChild() 等方法。在客户端看来,叶子对象与容器对象所提供的方法是一致的,客户端可以一致地对待所有的对象。缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的,叶子对象不应该能调用那些容器对象才能拥有的方法。
  • 安全组合模式:抽象构件 Component 中没有声明任何用于管理成员对象的方法,而是在 Composite 类中声明并实现这些方法。对于叶子对象,客户端不可能调用到这些方法。缺点是不够透明,客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件,要判断某一个对象是是叶子和容器再使用。

动机:在软件在某些情况下,客户代码过多地依赖于对象容器复杂的内部实现结构,对象容器丙部实现结构(而非抽象接口)的变化将引起客户代码的频繁变化,带来了代码的维护性、扩展性等弊端。

应用实例:操作系统的目录结构中,包含文件和文件夹两类不同的元素,在文件夹中可以包含文件,还可以继续包含子文件夹,在文件中不能再包含子文件或者子文件夹,文件夹对应着容器(Container)对象,文件对应着叶子(Leaf)对象。

结构类图:

透明组合模式:

image-20201216104601381

安全组合模式:

image-20201216104652582

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//透明组合模式
//抽象构件
public abstract class Component {
public abstract void add(Component c); //增加成员
public abstract void remove(Component c); //删除成员
public abstract Component getChild(int i); //获取成员
public abstract void operation(); //业务方法
}
//叶子构件
public class Leaf extends Component {
public void add(Component c) {
//异常处理或错误提示
}

public void remove(Component c) {
//异常处理或错误提示
}

public Component getChild(int i) {
//异常处理或错误提示
return null;
}

public void operation() {
//叶子构件具体业务方法的实现
}
}
//容器构件
public class Composite extends Component {
private ArrayList<Component> list = new ArrayList<Component>();

public void add(Component c) {
list.add(c);
}

public void remove(Component c) {
list.remove(c);
}

public Component getChild(int i) {
return (Component)list.get(i);
}

public void operation() {
//容器构件具体业务方法的实现,将递归调用成员构件的业务方法
for(Object obj:list) {
((Component)obj).operation();
}
}
}

迭代器模式 Iterator

迭代器模式:提供一种方法顺序访问一个聚合对象中各个元素,且不用暴露该对象的内部表示。

Iterator Pattern: Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.——《Design Patterns: Elements of Reusable Object-Oriented Software》

迭代器模式又称为游标(Cursor)模式。将遍历数据的行为从聚合对象中分离出来,封装在迭代器对象中,由迭代器来提供遍历聚合对象内部数据的行为,简化聚合对象的设计,更符合单一职责原则。客户端无须了解聚合对象的内部结构即可实现对聚合对象中成员的遍历,还可以根据需要很方便地增加新的遍历方式。Java 中迭代器常常和泛型一起配合使用,保证了编译时的正确,编译后泛型类型会被擦除。

动机:在软件构建过程中,集合对象内部结构常常变化各异。但对于这些集合对象,我们希望在不暴露其内部结构的同时,可以让外部客户代码透明地访问其中包含的元素;同时这种“透明遍历”也为“同一种算法在多种集合对象上进行操作”提供了可能。

应用实例:java.util.Iterator 这个类提供 Java 集合对迭代器的支持。Java 1.5 出现的增强的 for 循环语句也是利用到了迭代器。

image-20201216114755094

结构类图:

image-20201216114429923

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//抽象迭代器
public interface Iterator {
public void first(); //将游标指向第一个元素
public void next(); //将游标指向下一个元素
public boolean hasNext(); //判断是否存在下一个元素
public Object currentItem(); //获取游标指向的当前元素
}
//具体迭代器
public class ConcreteIterator implements Iterator {
private ConcreteAggregate objects; //维持一个对具体聚合对象的引用,以便于访问存储在聚合对象中的数据
private int cursor; //定义一个游标,用于记录当前访问位置
public ConcreteIterator(ConcreteAggregate objects) {
this.objects=objects;
}

public void first() { ...... }

public void next() { ...... }

public boolean hasNext() { ...... }

public Object currentItem() { ...... }
}
//抽象聚合类
public interface Aggregate {
Iterator createIterator();
}
//具体聚合类
public class ConcreteAggregate implements Aggregate {
......
public Iterator createIterator() {
return new ConcreteIterator(this);
}
......
}

职责链模式 Chain of Responsibility

职责链模式:避免将一个请求的发送者与接收者耦合在一起,让多个对象都有机会处理请求。将接收请求的对象连接成一条链,并且沿着这条链传递请求,直到有一个对象能够处理它为止。

Chain of Responsibility Pattern: Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.——《Design Patterns: Elements of Reusable Object-Oriented Software》

职责链模式将请求的处理者组织成一条链,并让请求沿着链传递,由链上的处理者对请求进行相应的处理。客户端无须关心请求的处理细节以及请求的传递,只需将请求发送到链上,将请求的发送者和请求的处理者解耦。职责链可以是一条直线、一个环或者一个树形结构,最常见的职责链是直线型,即沿着一条单向的链来传递请求。

纯与不纯的职责链模式

  • 纯的职责链模式:一个具体处理者对象只能在两个行为中选择一个:要么承担全部责任,要么将责任推给下家,一个请求必须被某一个处理者对象所接收,如果请求到职责链的末尾仍得不到处理,应该有一个合理的缺省处理机制,不能出现某个请求未被任何一个处理者对象处理的情况。
  • 不纯的职责链模式:允许某个请求被一个具体处理者部分处理后向下传递,或者一个具体处理者处理完某请求后其后继处理者可以继续处理该请求,一个请求可以最终不被任何处理者对象所接收并处理。

动机:在软件构建过程中,一个请求可能被多个对象处理,但是每个请求在运行时只能有一个接受者,如果显式指定,将必不可少地带来请求发送者与接受者的紧耦合。

应用实例:辅导员、系主任、院长、校长都可以处理奖学金申请表,他们构成一个处理申请表的链式结构,申请表沿着这条链进行传递,这条链就称为职责链。JavaScript 的事件浮升(Event Bubbling)处理机制。

结构类图:

image-20201216120528500

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//抽象处理者
public abstract class Handler {
//维持对下家的引用
protected Handler successor;

public void setSuccessor(Handler successor) {
this.successor=successor;
}

public abstract void handleRequest(String request);
}
//具体处理者
public class ConcreteHandler extends Handler {
public void handleRequest(String request) {
if (请求满足条件) {
//处理请求
}
else {
this.successor.handleRequest(request); //转发请求
}
}
}
//客户端调用职责链代码段
……
Handler handler1, handler2, handler3;
handler1 = new ConcreteHandlerA();
handler2 = new ConcreteHandlerB();
handler3 = new ConcreteHandlerC();
//创建职责链,给处理者指定下家
handler1.setSuccessor(handler2);
handler2.setSuccessor(handler3);
//发送请求,请求对象通常为自定义类型
handler1.handleRequest("请求对象");
……

行为变化模式

在组件的构建过程中,组件行为的变化经常导致组件本身剧烈的变化。“行为变化”模式将组件的行为和组件本身进行解耦,从而支持组件行为的变化,实现两者之间的松耦合。

命令模式 Command

命令模式:将一个请求封装为一个对象,从而让你可以用不同的请求对客户进行参数化,对请求排队或者记录请求日志,以及支持可撤销的操作。

Command Pattern: Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.——《Design Patterns: Elements of Reusable Object-Oriented Software》

命令模式别名为动作(Action)模式或事务(Transaction)模式。命令模式的本质是对请求进行封装,一个请求对应于一个命令,将发出命令的责任和执行命令的责任分开。命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求如何被接收、操作是否被执行、何时被执行,以及是怎么被执行的。

动机:在软件构建过程中,“行为请求者”与“行为实现者”通常呈现—种“紧耦合”。但在某些场合——比如需要对行为进行“记录.撤销/重做(undo/redo)、事务”等处理,这种无法抵御变化的紧耦合是不合适的。

应用实例:

  • 自定义快捷键:通过修改配置文件,更换具体的命令类,使得按下相同的快捷键可以调用不同的事件处理类方法。快捷键是请求发送者,事件处理类是请求的最终接收者。发送者与接收者之间引入了新的命令对象,将发送者的请求封装在命令对象中,再通过命令对象来调用接收者的方法。
  • 命令队列:当一个请求发送者发送一个请求时,有不止一个请求接收者产生响应,这些请求接收者将逐个执行业务方法,完成对请求的处理,增加一个 CommandQueue 类,由该类负责存储多个命令对象,而不同的命令对象可以对应不同的请求接收者。
  • 记录请求日志:将请求的历史记录保存下来,通常以日志文件(Log File)的形式永久存储在计算机中,将发送请求的命令对象通过序列化写到日志文件中,命令类必须实现接口 Serializable
  • 撤销操作:可以通过对命令类添加上 undo()redo() 方法,使得系统支持撤销(Undo)操作和恢复(Redo)操作。
  • 宏命令:宏命令(Macro Command)又称为组合命令(Composite Command),它是组合模式和命令模式联用的产物。宏命令是一个具体命令类,它拥有一个集合,在该集合中包含了对其他命令对象的引用。当调用宏命令的 execute() 方法时,将递归调用它所包含的每个成员命令的 execute() 方法。一个宏命令的成员可以是简单命令,还可以继续是宏命令,执行一个宏命令将触发多个具体命令的执行,从而实现对命令的批处理。

结构类图:

image-20201216121857882

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//抽象命令类
public abstract class Command {
public abstract void execute();
}
//具体命令类
public class ConcreteCommand extends Command {
private Receiver receiver; //维持一个对请求接收者对象的引用

public void execute() {
receiver.action(); //调用请求接收者的业务处理方法action()
}
}
//请求接收者类
public class Receiver {
public void action() {
//具体操作
}
}
//调用者(请求发送者)类
public class Invoker {
private Command command;

//构造注入
public Invoker(Command command) {
this.command = command;
}

//设值注入
public void setCommand(Command command) {
this.command = command;
}

//业务方法,用于调用命令类的execute()方法
public void call() {
command.execute();
}
}

访问器模式 Visitor

访问者模式:表示一个作用于某对象结构中的各个元素的操作。访问者模式让你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

Visitor Pattern: Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.——《Design Patterns: Elements of Reusable Object-Oriented Software》

访问者模式的对象结构(Object Structure)中存储了多种不同类型的元素信息,将有关元素对象的访问行为集中到一个访问者对象中,不同的具体访问者对这些元素有不同的处理方式。增加新的元素处理方式只需要增加一个新的具体访问者,但是如果要增加新的具体元素类会破坏 Visitor 的封装。类似于抽象工厂中产品族和产品等级结构的关系,对开闭原则具有倾斜性,适用于对象结构中元素类稳定,对元素的处理操作经常变化的场景。

动机:在软件构建过程中,由于需求的改变,某些类层次结构中常常需要增加新的行为(方法),如果直接在基类中做这样的更改,将会合子类带来很繁重的变更负担,甚至破坏原有设计。

应用实例:公司的人力资源部负责汇总每周员工工作时间,财务部负责计算每周员工工资。员工列表对应对象结构,全职员工和兼职员工对应着具体元素类,人力资源部和财务部对应着不同的具体访问者,分别对员工列表有着不同的操作:计算工作时间和计算工资。

image-20201216140651064

结构类图:

image-20201216130631797

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//抽象访问者类
public abstract class Visitor {
public abstract void visit(ConcreteElementA elementA);
public abstract void visit(ConcreteElementB elementB);

public void visit(ConcreteElementC elementC) {
//元素ConcreteElementC操作代码
}
}
//具体访问者类
public class ConcreteVisitor extends Visitor {
public void visit(ConcreteElementA elementA) {
//元素ConcreteElementA操作代码
}

public void visit(ConcreteElementB elementB) {
//元素ConcreteElementB操作代码
}
}
//抽象元素类
public interface Element {
public void accept(Visitor visitor);
}
//具体元素类
public class ConcreteElementA implements Element {
//主动去调用访问者的处理操作来处理元素自己
public void accept(Visitor visitor) {
visitor.visit(this);
}

public void operationA() {
//业务方法
}
}
//对象结构类
public class ObjectStructure
{
//定义一个集合用于存储元素对象
private ArrayList<Element> list = new ArrayList<Element>();

//接受访问者的访问操作
public void accept(Visitor visitor) {
//遍历对象结构中的每一个元素执行访问者的操作
for(Element e : list) {
e.accept(visitor);
}
}

public void addElement(Element element) {
list.add(element);
}
public void removeElement(Element element) {
list.remove(element);
}
}

领域规则模式

在特定领域中,某些变化虽然频繁,但可以抽象为某种规则。这时候,结合特定领域,将问题抽象为语法规则,从而给出在该领域下的一般性解决方案。

解释器模式 Interpreter

解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。

Interpreter Pattern: Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.——《Design Patterns: Elements of Reusable Object-Oriented Software》

解释器模式定义一套文法规则来实现对这些语句的解释,即使用规定格式和语法的代码设计一个自定义语言。解释器模式适合简单的文法规则,对于复杂的文法需要使用语法分析生成器工具。

动机:在软件构建过程中,如果某一特定领域的问题比较复杂,类似的结构不断重复出现,如果使用普通的编程方式来实现将面临非常频频繁的变化。

应用实例:解析字符串形式的四则远算表达式。

结构类图:

image-20201216131849497

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//抽象表达式类
public abstract class AbstractExpression {
public abstract void interpret(Context ctx);
}
//终结符表达式类
public class TerminalExpression extends AbstractExpression {
public void interpret(Context ctx) {
//终结符表达式的解释操作
}
}
//非终结符表达式类
public class NonterminalExpression extends AbstractExpression {
private AbstractExpression left;
private AbstractExpression right;

public NonterminalExpression(AbstractExpression left,AbstractExpression right) {
this.left=left;
this.right=right;
}

public void interpret(Context ctx) {
//递归调用每一个组成部分的interpret()方法
//在递归调用时指定组成部分的连接方式,即非终结符的功能
}
}
//上下文环境类
public class Context {
private HashMap<String, String> map = new HashMap<String, String>();

public void assign(String key, String value) {
//往环境类中设值
map.put(key, value);
}

public String lookup(String key) {
//获取存储在环境类中的值
return map.get(key);
}
}

参考

  1. B站视频:C++ 设计模式 Design Patterns李建忠
  2. 刘伟:《Java 设计模式》