设计模式。本笔记来源于:《Java Design Pattern》一书。

目录

  1. 设计原则
  2. 简单工厂模式/静态工厂模式
  3. 工厂方法模式/虚拟构造模式/多态工厂模式
  4. 抽象工厂模式/Kit 模式
  5. 单例模式 - 懒汉模式/饿汉模式/IoDH
  6. 原型模式 - 浅克隆与深克隆
  7. 建造者模式 - 复杂对象的组装与创建
  8. 适配器模式 - 不兼容结构的协调
  9. 桥接模式/柄体模式/接口模式 - 处理多维度变化
  10. 组合模式 - 树形结构的处理
  11. 装饰模式 - 扩展系统功能
  12. 外观模式
  13. 享元模式 - 实现对象的复用
  14. 代理模式
  15. 责任链模式 - 请求的链式处理
  16. 命令模式 - 请求发送者和接收者解耦
  17. 解释器模式 - 自定义语言的实现
  18. 迭代器模式 - 遍历聚合对象中的元素
  19. 中介者模式 - 协调多个对象之间的交互
  20. 备忘录模式 - 撤销功能的实现
  21. 观察者模式 - 对象间的联动
  22. 状态模式 - 处理对象的多种状态及其相互转换
  23. 策略模式 - 算法的封装与切换
  24. 模板方法模式
  25. 访问者模式 - 操作复杂对象结构

常用设计模式一览表

类型 模式名称 英文名
创建型模式 单例模式 Single Pattern
Creational Pattern 简单工厂模式 Simple Factory Pattern
工厂方法模式 Factory Method Pattern
抽象工厂模式 Abstract Factory Pattern
原型模式 Prototype Pattern
建造者模式 Builder Pattern
结构型模式 适配器模式 Adapter Pattern
Structural Pattern 桥接模式 Bridge Pattern
组合模式 Composite Pattern
装饰模式 Decorator Pattern
外观模式 Facade Pattern
享元模式 Flyweight Pattern
代理模式 Proxy Pattern
行为型模式 责任链模式 Chain Of Responsibility Pattern
Behavioral Pattern 命令模式 Command Pattern
解释器模式 Interpreter Pattern
迭代器模式 Iterator Pattern
中介者模式 Mediator Pattern
备忘录模式 Memento Pattern
观察者模式 Observer Pattern
状态模式 State Pattern
策略模式 Strategy Pattern
模板方法模式 Template Method Pattern
访问者模式 Visitor Pattern

面向对象设计原则

设计原则名称 英文名称 定义
单一职责原则 Single Responsibility Principle,SRP 一个类只负责一个功能领域中的相应职责
开闭原则 Open-Closed Principle,OCP 软件实体应对扩展开放,而对修改关闭
里氏替换原则 Liskov Substitution Principle,LSP 所有引用基类对象的地方能够透明地使用其子类的对象
依赖倒置原则 Dependence Inversion Principle,DSP 抽象不应该依赖于细节,细节应该依赖于抽象
接口隔离原则 Interface Segregation Principle,ISP 使用多个专门的接口,而不使用单一的总接口
合成复用原则 Composite Reuse Principle,CRP 尽量使用对象组合,而不是继承来达到复用的目的
迪米特法则 Law of Demetr,LoD 一个软件实体应当尽可能少地与其他实体发生相互作用

第一章 简单工厂模式

一、设计图表库

设计一个图表库,用于为系统提供各种不同外观的图表,如柱状图、饼状图、折线图等。

1.1 初步设计

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 class Chart {

private String type; // 图表类型

public Chart(Object[][] data, String type) {
this.type = type;
if (type.equalsIgnoreCase("histogram")) {
// 初始化柱状图
}
if (type.equalsIgnoreCase("pie")) {
// 初始化饼状图
}
if (type.equalsIgnoreCase("line")) {
// 初始化折线图
}
}

public void display() {
if (type.equalsIgnoreCase("histogram")) {
// 显示柱状图
}
if (type.equalsIgnoreCase("pie")) {
// 显示饼状图
}
if (type.equalsIgnoreCase("line")) {
// 显示折线图
}
}
}

1.2 存在的问题

该类在设计时存在如下几个问题:

  1. Chart 类中包含许多 if…else 代码块,代码冗长,难以阅读、维护和测试;大量的条件判断还将影响系统的性能
  2. Chart 类职责过重,负责初始化和显示所有图表对象。违反了“单一职责原则”,不利于类的重用和维护;而且将大量的对象初始化代码写在构造函数中,在初始化对象时需要条件判断,降低了对象创建的效率
  3. 违反了“开闭原则”,当需要增加新类型的图表时,必须修改 Chart 对象的代码
  4. 客户端只能通过 new 关键字来直接创建 Chart 对象,与客户端耦合度较高,对象的创建和使用无法分离
  5. 客户端中缺少其它初始化设置,如果在客户端对图表的颜色、高度等进行设置,则会在每次创建对象时都会出现,导致代码的重复

1.3 解决思路

引入工厂类,进行抽象与拆分

二、简单工厂模式

2.1 设计流程

  1. 将需要创建的各种不同对象的相关代码封装到不同的类中,这些类称为具体产品类
  2. 将它们公共的代码进行抽象和提取后封装在一个抽象产品类中,每个具体产品类都是抽象产品类的子类
  3. 提供一个工厂类用于创建各种产品,在工厂类中提供一个创建产品的工厂方法,该方法可以根据所传入的参数不同创建不同的具体对象
  4. 客户端只需调用工厂类的方法并传入相应的参数即可得到一个产品对象

2.2 简单工厂模式模式的定义

定义一个工厂类,可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。因为在简单工厂模式中用于创建实例的方法是静态方法,
因此简单工厂模式又被称为静态工厂模式,属于类创建型模式。

2.3 简单工厂模式中的几个角色

  1. Factory(工厂角色)
    1. 即工厂类,是简单工厂模式的核心,负责实现创建所有产品实例的内部逻辑;
    2. 工厂类可以被外界直接调用,创建所需的产品对象;
    3. 在工厂类中提供了静态的工厂方法,返回类型为抽象产品类;
  2. Product(抽象产品角色)
    1. 是工厂类所创建的对象的父类,封装各产品对象的公共方法
    2. 抽象产品类的引入,提供了系统的灵活性,使得在工厂类只需定义一个通用的工厂方法,因为所创建的具体产品对象都是其子类对象
  3. ConcreteProduct(具体产品角色)
    1. 是简单工厂模式的创建目标,所有被创建的对象都充当这个角色的某个类的实例。
    2. 每个具体产品角色都继承了抽象产品角色,需要实现在抽象产品中声明的抽象方法

三、具体实现

3.1 产品抽象类

1
2
3
4
5
6
7
/**
* 使用简单工厂模式时,需要先对产品类进行重构
* 根据实际情况设计一个产品层次结构,将所有产品类公共的代码移至抽象类,并声明一些抽象方法供具体产品类类实现
*/
public interface Chart {
void display();
}

3.2 具体产品类

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
/**
* 具体产品类实现了抽象产品类中声明的抽象业务方法,不同的具体产品类可以提供不同的实现
*/
public class HistogramChart implements Chart {
public HistogramChart() {
System.out.println("生成柱状图");
}

@Override
public void display() {
System.out.println("显示柱状图");
}
}

public class LineChart implements Chart {
public LineChart() {
System.out.println("生成折线图");
}

@Override
public void display() {
System.out.println("显示折线图");
}
}

public class PieChart implements Chart {
public PieChart() {
System.out.println("生成饼状图");
}

@Override
public void display() {
System.out.println("显示饼状图");
}
}

3.3 工厂方法类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 简单工厂模式的核心就是工厂类
* 通过工厂类的静态工厂方法来创建具体产品对象,而非直接通过new来创建产品对象
*/
public class ChartFactory {
public static Chart getChart(String type) {
switch (type) {
case "histogram":
return new HistogramChart();
case "pic":
return new PieChart();
case "line":
return new LineChart();
default:
return null;
}
}
}

3.4 客户端测试

1
2
3
4
5
6
7
8
9
/**
* 客户端测试代码
*/
public class DemoTest {
public static void main(String[] args) {
Chart pie = ChartFactory.getChart("pie");
pie.display();
}
}

四、总结

简单工厂模式提供了专门的工厂类用于创建对象,将对象的创建和对象的使用分离。

4.1 优点

  1. 工厂类包含必要的判断逻辑,可以决定何时创建哪一个实例。免除客户端直接创建产品对象的职责,而仅仅“消费”产品。实现了对象创建和使用的分离
  2. 客户端无需知道所创建的具体产品类的类名,只需要知道具体参数即可。
  3. 可以将具体参数引入配置文件,在不修改客户端代码的情况下增删具体产品类,在一定程序上提供了系统灵活度。

4.2 缺点

  1. 工厂类的职责过重,集中了所有产品的创建逻辑。一旦工厂类出现问题,整个系统都会受到影响
  2. 使用简单工厂模式势必会增加系统中类的个数,增加了系统的复杂度和理解难度
  3. 系统拓展难度,新增产品时必须修改工厂逻辑,在产品类型较多时,可能造成工厂逻辑过于复杂,不利于系统扩展和维护
  4. 简单工厂模式由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构

4.3 适用场景

  1. 工厂类负责创建的对象较少,因为需要创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂
  2. 客户端只知道传入工厂类的参数,对于如何创建对象并不关心

第二章 工厂方法模式

工厂方法模式又称为工厂模式、虚拟构造模式或多态工厂模式,是一种类创建型模式。

一、设计一个日志记录器

设计一个日志记录器,可以通过多种途径保存系统的运行日志,如文件记录或数据库记录等。

1.1 简单模式实现

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
// 抽象产品类
interface Logger{
void writeLog();
}
// 具体产品类
class FileLogger implements Logger{
@Override
void writeLog(){
System.out.println("文件日志记录");
}
}
class DatabaseLogger implements Logger{
@Override
void writeLog(){
System.out.println("数据库日志记录");
}
}
// 工厂类
class LoggerFactory{
public static Logger createLogger(String type){
switch(type){
case "file":
// 创建文件、初始化文件日志、等其他操作
return new FileLogger();
case "db":
// 连接数据库、创建数据库日志、等其他操作
return new DatabaseLogger();
default:
throw new UnSupportOperationException();
}
}
}

1.2 缺陷与问题

虽然简单工厂模式实现了对象的创建和使用分离,但是仍然存在以下问题:

  1. 工厂类过于庞大,包含了许多的条件判断,导致维护和测试难度增大
  2. 系统扩展不灵活,如果新增其他类型的日志记录器,必须修改静态工厂方法的业务逻辑,违反“开闭原则”

1.3 解决思路

简单工厂方法的缺陷在于违反了“开闭原则”,每次新增新类型时必须修改静态工厂方法的业务逻辑。所有的产品都由一个工厂来创建,工厂职责较重,业务逻辑较为复杂,具体产品和工厂类之间耦合度高。严重影响了系统的灵活性和扩展性。

通过引入工厂方法模式,将工厂与产品拆分,降低耦合,提高系统灵活性。

二、工厂方法模式

2.1 概述

在工厂方法模式中,不再提供一个统一的工厂类来创建所有的产品对象,而是针对不同的产品提供不同的工厂,系统提供一个与产品等级结构对应的工厂等级结构。

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

2.2 工厂方法模式中的角色

  1. Product(抽象产品):产品对象的公共父类
  2. ConcreteProduct(具体产品):实现抽象产品接口,由具体工厂创建,一一对应
  3. Factory(抽象工厂):在抽象工厂中,声明了工厂方法,用于返回一个产品。
    1. 抽象工厂是工厂方法模式的核心,所有创建对象的工厂类都必须实现该接口
  4. ConcreteFactory(具体工厂):抽象工厂的子类,实现了抽象工厂中定义的方法,通过调用工厂方法,返回一个具体产品类的实例。

工厂方法模式与简单方法模式的区别就在于,多了一个抽象工厂类。

在抽象工厂中声明工厂方法,有具体工厂类来创建子类,不同的具体工厂可以创建不同的具体产品。

在实际使用中,还可以在具体工厂中做一些初始化工作,如资源加载、环境配置等工作。

而客户端只需针对抽象工厂编程即可。

三、解决方案

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
// 抽象产品类
interface Logger{
void writeLog();
}
// 具体产品类
class FileLogger implements Logger{
@Override
void writeLog(){
System.out.println("文件日志记录");
}
}
class DatabaseLogger implements Logger{
@Override
void writeLog(){
System.out.println("数据库日志记录");
}
}
// 抽象工厂类
interfact Factory{
Logger createLogger();
}
// 具体工厂类
class FileLoggerFactory implement Factory {
@Override
public Logger createLogger(){
// 初始化环境等
return new FileLogger();
}
}
class DatabaseLoggerFactory implement Factory {
@Override
public Logger createLogger(){
// 初始化环境等
return new DatabaseLogger();
}
}
// 测试
public static void main(String[] args){
Factory factory = new FileLoggerFactory();
Logger logger = factory.createLogger();
logger.writeLog();
}

四、扩展

使用 反射与配置文件 扩展系统。

为了让系统拥有更好的灵活性和可扩展性,在客户端代码汇总不使用new关键字来创建工厂对象,而是将具体工厂类的类名存储在配置文件中,通过读取配置文件获取类名字符串,再使用 Java反射机制,根据类名字符串来生成对象。

五、隐藏工厂方法

为了进一步简化客户端的使用,还可以对客户端隐藏工厂方法,在抽象工厂中直接调用产品类的业务方法。

1
2
3
4
5
6
7
abstract class Factory{
public void writeLog(){
Logger logger = this.createLogger();
logger.writeLog();
}
abstract Logger createLogger();
}

这样,客户端直接调用抽象工厂的方法即可调用具体的产品业务方法。

六、工厂方法模式总结

优点

  1. 由工厂方法来创建客户所需的产品,以及隐藏了具体产品类的实例化过程,客户只需关心所需产品对应的工厂,无需关系创建细节,甚至无需知道产品类的类名。
  2. 基于工厂角色和产品角色的多态性设计是工厂方法的关键。能让工厂可以自主确定创建何种产品对象,而创建对象的细节封装在具体工厂的内部。
  3. 符合“开闭原则”,向系统中新增产品时,无需修改抽象工厂和抽象产品提供的接口,无需修改客户端,只需要添加一个具体工厂和具体产品即可。

缺点

  1. 在添加新产品时,需要编写新的具体产品类,还需要提供对应的具体工厂类。系统中类的个数增加,提升了系统复杂度,更多的类需要编译和运行,会给系统带来一些额外开销。
  2. 由于考虑系统的可扩展性,需要引入抽象层,增加了系统抽象性和理解难度。且实现时可能用到DOM、反射等技术,增加系统实现难度

适用场景

  1. 客户端不知道具体所需的对象,只知道工厂即可。
  2. 抽象工厂类通过其子类来指定创建哪个对象。
    1. 抽象工厂类只需要提供一个创建工厂的接口,由其子类-具体工厂类来决定要创建的对象
    2. 利用面向对象的多态性和里氏替换原则,在程序运行时,由子类替换父类,从而使系统更容易扩展

七、练习

使用工厂方法模式设计一个程序来读取各种不同类型的图片格式,针对每一种图片格式都设计一个图片读取器,如GIF图片读取器用于读取GIF格式的图片、JPG图片读取器用于读取JPG格式的图片。需充分考虑系统的灵活性和可扩展性。


第三章 抽象工厂模式

在工厂方法模式中,通过引入工厂等级结构,解决了 简单工厂模式 中工厂类过于庞大,职责过重的问题。但是由于工厂方法模式中的每一个工厂只生产一类产品,导致系统中存在大量工厂类,增加系统开销。

因此我们需要将一些相关的产品组成一个“产品族”,由同一个工厂生产。

一、抽象工厂模式

1.1 设计一款界面皮肤库

设计一款界面皮肤库,可以通过菜单来选择皮肤,不同的皮肤将提供视觉效果不同的按钮、文本框、组合框等。

按照之前的 工厂方法模式 设计,如下:

工厂方法模式.jpg

当我们需要增加 Winter风格 的按钮时,只需要继承抽象工厂并生产出对应的按钮即可。该系统具备良好的灵活性和可扩展性,开发可以在不修改现有代码的基础上增加新的皮肤。

1.2 缺陷与问题

该设计模式提供了大量的工厂来创建具体的界面组件,可以灵活的配置风格,但是存在以下问题:

  1. 新增产品时,需要同时新增一个具体工厂类,类的个数成对增加,会增大系统的开销
  2. 由于某一种风格的按钮、文本框、组合框通常都是一起使用的,但是因为其工厂方法分离,可能会导致使用时的选择失误

1.3 解决方案

将一组产品类定义为一个“产品族”(如按钮、文本框、组合框统称为一个产品族),将之前一个产品对应一个具体工厂的模式改为-一个产品族对应一个产品工厂(如Spring风格工厂,能够生产Spring风格按钮、Spring风格文本框、Spring风格组合框)。

1.4 产品等级结构与产品族

  1. 产品等级结构
    1. 1.3 解决方案 中,抽象工厂与Spring风格工厂、Summer风格工厂之间的关系构成了一个产品等级结构
    2. 抽象工厂类是父类,具体工厂类是子类
    3. 产品等级扩展,只需要继续实现子类即可(如新增Winter风格工厂,用于生产Winter风格的按钮、文本框、组合框)
  2. 产品族:
    1. 即各个具体工厂生产的产品统称(如Spring风格工厂,能够生产Spring风格按钮、Spring风格文本框、Spring风格组合框)

当系统所提供的工厂生产的具体产品并不是一个简单的对象,而是位于多个不同产品等级结构、属于不同类型的具体产品时就可以使用抽象工厂模式。

二、抽象工厂模式

2.1 概述

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

抽象工厂模式又称为 Kit模式,属于对象创建型模式。

在抽象工厂模式中,每一个具体工厂都提供多个工厂方法,用于生产多种不同类型的产品。这些产品构成了一个产品族。

抽象工厂模式

从图中可以看出,如果需要新增其他风格的按钮、文本框,只需要新增一个具体工厂,由该工厂的多个工厂方法产出具体的产品。

同时,缺点也很明显,如果我们想新增图片类型产品,必须同时修改抽象工厂及其所有的具体工厂。

2.2 抽象工厂模式中的几个角色

  1. AbstractFactory - 抽象工厂
    1. 声明了一组用于创建一族产品的方法,每一个方法对应一种产品
  2. ConcreteFactory - 具体工厂
    1. 实现了在抽象工厂中声明的创建产品的方法,生成一组具体产品,这些产品构成一个产品族
  3. AbstractProduct - 抽象产品
    1. 每种产品的抽象对象
  4. ConcreteProduct 具体产品
    1. 由具体工厂生产的具体产品对象

2.3 具体实践

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
/**
* 手机游戏软件
* 针对 Symbian、Android、WindowsPhone 等多个智能手机平台,
* 提供不同的游戏操作控制和游戏界面控制
*/
public class Gaming {
// 抽象产品类
interface Operation{}
// 具体产品类
class SymbianOperation implements Operation{}
class AndroidOperation implements Operation{}
class WindowsPhoneOperation implements Operation{}

interface HML{}
class SymbianHML implements HML{}
class AndroidHML implements HML{}
class WindowsPhoneHML implements HML{}

// 抽象工厂类
interface Factory{
Operation getOperation();
HML getHML();
}
// 具体工厂类
class SymbianFactory implements Factory{
@Override
public Operation getOperation() {
return new SymbianOperation();
}
@Override
public HML getHML() {
return new SymbianHML();
}
}
class AndroidFactory implements Factory{
@Override
public Operation getOperation() {
return new AndroidOperation();
}
@Override
public HML getHML() {
return new AndroidHML();
}
}
class WindowsPhoneFactory implements Factory{
@Override
public Operation getOperation() {
return new WindowsPhoneOperation();
}
@Override
public HML getHML() {
return new WindowsPhoneHML();
}
}
}

三、“开闭原则”的倾斜性

在抽象工厂模式中,增加新的产品族很方便(如增加Winter类型的界面),但是增加新的产品等级结构很麻烦(如增加单选框类型的产品,需要修改抽象工厂及其所有的具体工厂),这种性质被称为“开闭原则”的倾斜性。

“开闭原则”要求系统对扩展开发,对修改封闭,通过扩展达到增强其功能的目的,对于设计多个产品族与多个产品等级结构的系统,其功能增强包括两方面:

  1. 增加产品族,很好地支持了“开闭原则”,只需新增具体产品和新增一个具体工厂即可
  2. 增加产品等级结构:违背了“开闭原则”,因为需要修改所有的抽象工厂与具体工厂才能实现扩展

四、总结

优点

  1. 抽象工厂模式隔离了具体类的生成,使客户不需要知道何时被创建
  2. 当一个产品族中的多个对象被设计为一起工作时,它能够保证客户端始终只使用一个产品族对象
  3. 增加新的产品族很方便,无须修改已有系统,符合“开闭原则”

缺点

  1. 增加新的产品等级结构时,违背了“开闭原则”

适用场景

  1. 系统不依赖于产品类的具体创建、组合及表达的细节
  2. 系统中有多于一个的产品族,且每次只使用其中某一个产品族
  3. 同一个产品族的产品配合使用
  4. 产品等级结构稳定,设计完成后,不再修改系统中新的产品等级结构

第四章 单例模式 - 确保对象的唯一性

单例模式用于确保对象的唯一性,为了确保系统中某一个类只有一个唯一实例,当这个唯一实例创建后,无法再创建一个同类型的其他对象。

一、概述

单例模式:确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。属于对象创建型模式。

1.2 单例模式中的角色

  • Singleton - 单例角色
    • 在单例类内容实现只生成一个实例,同时提供一个静态工厂方法用于获取该唯一实例
    • 为了防止外部实例化,将其构造函数设计为私有
    • 单例类内部顶一个Singleton类型的静态对象,作为外部共享的唯一实例

二、懒汉模式与饿汉模式

2.1 饿汉式单例类 - 线程安全

懒汉单例模式

1
2
3
4
5
6
7
class EagerSingleton{
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton(){}
public static EagerSingleton getInstance(){
return instance;
}
}

饿汉模式实现起来最为简单,在类加载时,静态变量 instance 就会被初始化,此时类的私有构造函数会被调用,单例类的唯一实例将被创建。

2.2 懒汉式单例类(一) - 线程安全

懒汉单例模式

懒汉模式能将类进行 延迟加载,即在需要使用时再加载实例,为了避免多线程调用 getInstance() 时实例化多个 LazySingleton,这里使用 synchronized 关键字进行加锁。

1
2
3
4
5
6
7
8
9
class LazySingleton{
private static LazySingleton instance = null;
private LazySingleton(){}
synchronized public static LazySingleton getInstance(){
if(instance == null)
instance = new LazySingleton();
return instance;
}
}

2.3 懒汉式单例类(二) - 线程不安全

虽然 synchronized 关键字进行了线程锁定,但是每次调用 getInstance() 方法时都会进行加锁操作,在多线程高并发场景下,会导致系统性能大大降低。

事实上,只需要在 new L:azySingleton() 时进行加锁即可,即第一次实例化实例时加锁。

1
2
3
4
5
6
7
8
public static LazySingleton getInstance(){
if(instance == null){
synchronized(LazySingleton.class){
instance = new LazySingleton();
}
}
return instance;
}

2.4 懒汉模式单例类(三 DoubleCheck) - 线程不安全

如果当两个线程同时进行了 if 条件之中,当线程A完成实例化之后,线程B继续执行,依旧会进行一次实例化。违背了单例模式的设计思想。

双重检测机制:因此我们需要在 synchronized 中在进行一次判断

1
2
3
4
5
6
7
8
9
10
public static LazySingleton getInstance(){
if(instance == null){
synchronized(LazySingleton.class){
if(instance == null){
instance = new LazySingleton();
}
}
}
return instance;
}

2.5 懒汉模式单例类(四 volatile+DoubleCheck) - 线程安全

上述代码看起来可能没什么问题,但是

instance = new LazySingleton() 在内存中的指令顺序应该为:

  1. instance 分配内存地址
  2. 实例化 LazySingleton
  3. 将实例化地址指向instance

因为 JVM 的指令重排和优化,可能会导致以上执行顺序与所想像的不一致。最终导致单例出现问题,因此我们在静态实例 instance 前,加上 volatile 关键词,避免 instance 的实例化发生指令重排。

因此完整的懒汉模式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class LazySingleton{
// 使用volatile 修改时静态实例,避免指令重排序
private volatile static LazySignleton instance = null;
// 构造方法私有化,防止外部实例
private LazySingleton(){}
// 静态工厂类,用于返回唯一实例,且使用延迟加载技术
public static LazySingleton getInstance(){
// 第一重检查,只有当instance 为空时才进行实例化
if(instance == null){
// 实例化时对当前类进行加锁,避免多线程同时实例化
synchronized(LazySingleton.class){
// 第二重检查,避免当前线程通过第一重检查后其他线程已实例化过
if(instance == null){
instance = new LazySingleton();
}
}
}
return instance;
}
}

缺点也很明显:1. volatile 关键词虽然能保证实例避免重排,但是只在1.5以上的版本才能生效 2. volatile关键词会屏蔽 JVM 的一些代码优化,可能导致系统运行效率降低

三、一种更好的单例实现方法

3.1 懒汉模式与饿汉模式的缺点

饿汉模式:不能实现延迟加载,不管实例是否使用都将占据内存

饿汉模式:繁琐的线程安全控制,且性能会受到一定影响

3.2 静态内部类单例 - IoDH - 线程安全

1
2
3
4
5
6
7
8
9
10
11
12
class Signleton{
private Singleton(){}
// 静态内部类只有在第一次调用时才会进行加载
private static class HolderClass{
// 因为instance 不是Singleton的成员变量,所以不会在Singleton加载时实例化
private final static Singleton instance = new Singleton();
}
// 通过JVM特性来保证线程安全性,确保成员变量只能初始化一次
public static Singleton getInstance(){
return HolderClass.instance;
}
}

Initialization Demand Holder 技术,在单例类内部增加一个静态内部类,在该内部类中创建单例对象,再将单例对象通过 getInstance() 方法返回给外部使用。

但是IoDH依赖于JVM的特性,并不适用于所有语言。但是IoDH既可以实现延迟加载,又可以保证线程安全,又不影响性能,不失为一种最好的Java语言单例模式。

四、总结

优点

  1. 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,严格控制客户如何访问
  2. 节约系统资源。因为内存中只存在一个对象,无需频繁的创建、销毁对象
  3. 允许可变数目的实例。可以通过变种,让getInstance返回至多几个实例

缺点

  1. 由于单例类没有抽象层,很难进行扩展
  2. 单例类职责过重,违背了“单一职责原则”。因为单例类即是工厂角色,提供工厂方法,又是产品角色,包含一些业务方法。将产品的创建和产品的功能融合到了一起
  3. 许多面向对象语言(如Java、C#)的自动垃圾回收技术,如果实例化的共享变量长时间不被使用。GC 会自动销毁并回收资源,导致单例对象状态的丢失。

适用场景

  1. 系统只需要一个实例对象。如资源消耗过大的对象、资源管理器等只能实例一次的对象
  2. 客户调用类的单个实例只允许使用一个公共访问点,不能通过其他途径访问该实例。

第五章 原型模式 - 对象的克隆

原型模式:使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。属于一种对象创建型模式。

一、概述

1.1 原理

原型模式的工作原理很简单:将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝自己来实现创建过程。由于在软件系统中我们经常会遇到需要创建多个相同或者相似对象的情况,因此原型模式在真实开发中的使用频率还是非常高的。原型模式是一种“另类”的创建型模式,创建克隆对象的工厂就是原型类自身,工厂方法由克隆方法来实现。

1.2 原型模式中的几个角色

  1. Prototype - 抽象原型类
    1. 声明克隆方法的接口,是所有具体原型类的公共父类
  2. ConcretePrototype - 具体原型类
    1. 实现抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象
  3. Client - 客户类
    1. 让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。

1.3 案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Getter
@Setter
class WeeklyLog implements Clonealbe{
private String name;
private String date;
private String content;

publci WeekLog clone(){
Object obj = null;
try{
obj = super.clone();
return (WeekLog)obj;
} catch (CloneNotSupportedException e){
System.out.println("不支持复制");
return null;
}
}
}

二、浅克隆与深克隆

2.1 浅克隆

在浅克隆中,如果原型对象的成员是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象。也就是说,原型对象的引用类型变量与克隆对象中的变量指向同一内存地址。

即值类型成员变量会被复制,但是引用成员变量不会。

2.2 深克隆

在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象。

如果需要实现深克隆,可以通过序列化(Serialization)等方式实现:将对象写入到流,再从流中读出。

Java语言提供的 Cloneable 接口和 Serializable 接口都是空接口,也称为标识接口。标识接口无任何方法定义,只提供告诉JRE这些接口的实现类是否支持克隆、序列化等功能。

三、总结

优点

  1. 当创建新对象较为复杂时,可以使用原型模式简化对象的创建过程,复制一个已有实例以提高新实例的创建效率
  2. 扩展性较好,由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类写在配置文件中
  3. 原型模式提供了简化的创建结构,工厂方法模式需要与产品等级结构对应的工厂等级结构,而原型模式则直接通过封装在原型类中的克隆方法实现
  4. 深克隆方式可以保存对象的状态

缺点

  1. 需要每个类配置一个克隆方法,且位于类内部,当对类进行改造时需要修改源码,违反了“开闭原则”
  2. 实现深克隆时较为复杂,对象的每一层子对象都需要进行深克隆

适用场景

  1. 创建新对象成本较大(如初始化时间长、占用CPU资源多或网络资源占用多),新对象能够通过原型模式进行复制,复制相似对象再进行修改
  2. 可以通过原型模式+备忘录模式,来实现保存对象的状态
  3. 避免使用工厂模式来创建对象

第六章 建造者模式 - 复杂对象的组装与创建

一、设计一个游戏角色

作为 RPG 游戏的一个重要组成部分,游戏角色拥有其特定的性别、脸型、服装、发型等外部特性。

无论何种造型的游戏角色,它们的创建步骤都大同小异,都需要逐步创建其组成部分,再将各组成部分装配成一个完整的游戏角色。

而建造者模式就是为了解决,各个组件组合为复杂对象的问题。

建造者模式

二、建造者模式

2.1 概述

建造者模式,将客户端与包含多个组成部分的复杂对象的创建过程分离,客户端无须知道复杂对象的内部组成部分与装配方式,只需要知道所需建造者的类型即可。

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

2.2 建造者模式中的几个角色

  1. Builder - 抽象建造者
    1. 为创建一个产品对象的各个部件指定抽象接口
    2. 一般声明两类方法:一类方法是buildPartX(),用于创建复杂对象的各个部件;另一类是getResult(),用于返回复杂对象
    3. 抽象类或接口
  2. ConcreteBuilder - 具体建造者
    1. 实现了Builder接口,实现各个部件的具体构造和装配方法
  3. Product - 产品角色
    1. 被构建的复杂对象,包含多个组成部件
    2. 由具体建造者创建该产品的内部表示并定义它的装配过程
  4. Director - 指挥者
    1. 又称为导演类,负责安排复杂对象的建造次序
    2. 客户端一般只需与指挥者交互,由客户端确定具体建造者的类型,并实例化具体建造者对象,然后通过指挥者类的构造函数或setter方法将对象传入指挥类中

2.3 复杂对象、建造者与指挥者

1
2
3
4
5
6
// 复杂对象,主要是包含多种不同类型的成员属性(部件)
class Product {
private String partA;
private Object partB;
private User partC;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
abstract class Builder{
// 创建产品对象
protected Product product = new Product();

// buildPartX() 方法为产品对象的成员属性设值
public abstract void buildPartA();
public abstract void buildPartB();
public abstract void buildPartC();

// getResult 返回复杂对象
public Product getResult(){
return product;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Director {
private Builder builder;

/**
* 通过构造方法或者setter方法注入一个抽象建造者,由调用者确定使用何种类型的建造者
*/
public Direct(Builder builder){
this.builder = builder;
}

/**
* construct 方法中调用builder对象的构造部件方法,返回一个产品对象
*/
public Product construct() {
build.buildPartA();
build.buildPartB();
build.buildPartC();
return builder.getResult();
}
}

需要扩展新的建造者时,只需要重新实现抽象建造者即可,无需修改源代码,系统扩展十分方便。

在客户端代码中,无需关系产品对象的具体组装过程,只需要指定具体建造者的类型即可。

2.4 建造者模式的使用

1
2
3
Builder builder = new ConcreteBuilder();
Director director = new Director(builder);
Product product = director.construct();

2.5 建造者模式与抽象工厂模式

建造者模式与抽象工厂模式很类似:

抽象工厂模式返回一系列相关的产品,而建造者模式返回一个完整的复杂产品。

抽象工厂模式中,客户端通过选择具体工厂来生成所需对象,而建造者模式中,客户端通过指定具体建造者类型并指导Director类去生成对象,侧重于一步步构建复杂对象,然后将结果返回。

类似于:抽象工厂生产出不同类型的汽车配件,而建造者模式就是汽车组装厂。

三、完整案例

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import lombok.Getter;
import lombok.Setter;

// 角色类 - 复杂产品
@Getter
@Setter
public class Actor {
private String type; // 角色类型
private String sex; // 性别
private String face; // 脸型
private String costume; // 服装
private String hairStyle; // 发型
}

// 角色建造器 - 抽象建造者
abstract class ActorBuilder {
protected Actor actor = new Actor();

public abstract void buildType();

public abstract void buildSex();

public abstract void buildFace();

public abstract void buildCostume();

public abstract void buildHairStyle();

// 工厂方法,返回一个完整的游戏角色对象
public Actor createActor() {
return actor;
}
}

// 天使角色建造器 - 具体建造者
class AngelBuilder extends ActorBuilder {

@Override
public void buildType() {
actor.setType("天使");
}

@Override
public void buildSex() {
actor.setSex("女");
}

@Override
public void buildFace() {
actor.setFace("漂亮");
}

@Override
public void buildCostume() {
actor.setCostume("白裙子");
}

@Override
public void buildHairStyle() {
actor.setHairStyle("披肩长发");
}
}

// 恶魔角色建造器 - 具体建造者
class DevilBuilder extends ActorBuilder {

@Override
public void buildType() {
actor.setType("恶魔");
}

@Override
public void buildSex() {
actor.setSex("男");
}

@Override
public void buildFace() {
actor.setFace("丑陋");
}

@Override
public void buildCostume() {
actor.setCostume("丑陋");
}

@Override
public void buildHairStyle() {
actor.setHairStyle("光头");
}
}

// 游戏角色创建控制器 - 指挥者
class ActorController {
// 逐步构建复杂产品对象
public Actor construct(ActorBuilder buidler) {
buidler.buildType();
buidler.buildSex();
buidler.buildFace();
buidler.buildCostume();
buidler.buildHairStyle();
return buidler.createActor();
}
}

// 测试客户端,调用指挥者通过具体建造者来构建完整的复杂对象
class Client {
public static void main(String[] args) {
ActorBuilder actorBuilder = new AngelBuilder();
ActorController actorController = new ActorController();
Actor actor = actorController.construct(actorBuilder);
System.out.println(actor.getType() + ":" + actor.getFace());
}
}

四、扩展

4.1 省略Director指挥者

为了简化系统结构,可以将Director与抽象建造者Builder合并,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 角色建造器 - 抽象建造者
abstract class ActorBuilder {
protected static Actor actor = new Actor();

public abstract void buildType();

public abstract void buildSex();

public abstract void buildFace();

public abstract void buildCostume();

public abstract void buildHairStyle();

// 工厂方法,返回一个完整的游戏角色对象
public static Actor construct(ActorBuilder buidler) {
buidler.buildType();
buidler.buildSex();
buidler.buildFace();
buidler.buildCostume();
buidler.buildHairStyle();
return actor;
}
}

4.2 钩子方法

建造者模式除了逐步构建一个复杂产品对象外,还可以通过Director类来精细控制产品的创建过程,如增加一个钩子方法来控制是否构建某个buildPartX()的调用。

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
abstract class ActorBuilder{
protected Actor actor = new Actor();

public abstract void buildType();

public abstract void buildSex();

public abstract void buildFace();

public abstract void buildCostume();

public abstract void buildHairStyle();

// 钩子方法 - 是否是光头,true则不进行buildHairStyle
public boolean isBareheaded(){
return false;
}
}

// 游戏角色创建控制器 - 指挥者
class ActorController {
// 逐步构建复杂产品对象
public Actor construct(ActorBuilder buidler) {
buidler.buildType();
buidler.buildSex();
buidler.buildFace();
buidler.buildCostume();
if(!isBareheaded){
buidler.buildHairStyle();
}
return buidler.createActor();
}
}

五、总结

优点

  1. 在建造者模式中,客户端不必知道产品内部组成细节,将产品本身和产品的创建过程解耦,使得相同创建过程能够创建出不同的产品对象
  2. 每个具体建造者都相对独立,可以方便地替换或增加具体建造者,符合“开闭原则”
  3. 将复杂产品的创建步骤分解在不同方法中,使创建过程更清晰,也更方便程序控制创建的过程

缺点

  1. 建造者模式所创建的产品具有许多共同点,其组成部分相似。如果产品间差异过大,则不适合建造者模式,使用范围有限
  2. 如果产品内部变化复杂,会导致需要许多具体建造者来实现该变化,会导致系统庞大,增加系统理解难度和运行成本

适用场景

  1. 需要生成的产品对象有复杂的内部结构,即多个成员属性
  2. 产品对象的属性相互依赖,需要指定其生成顺序
  3. 对象的创建过程独立于创建该对象的类。在建造者模式中,创建过程封装在指挥者类中,而不再建造者和客户端中
  4. 隔离复杂对象的创建和使用,且相同的创建过程能够创建不同的产品

六、练习

Sunny软件公司欲开发一个视频播放软件,为了给用户使用提供方便,该播放软件提供多种界面显示模式,如完整模式、精简模式、记忆模式、网络模式等。在不同的显示模式下主界面的组成元素有所差异,如在完整模式下将显示菜单、播放列表、主窗口、控制条等,在精简模式下只显示主窗口和控制条,而在记忆模式下将显示主窗口、控制条、收藏列表等。尝试使用建造者模式设计该软件。

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/**
* 视频播放显示界面 - 具体产品
* 具体播放界面,包含显示菜单、播放列表、主窗口、控制条
* 可能包含的显示模式,完整模式、精简模式、网络模式
*/
@Setter
@Getter
public class TechWeb {
private String menu; // 菜单
private String list; // 播放列表
private String window; // 主窗口
private String control; // 控制条
}

/**
* 显示模式 - 抽象建造者
*/
abstract class ShowPattern {
protected TechWeb techWeb = new TechWeb();

public abstract void buildMenu();
public abstract void buildList();
public abstract void buildWindow();
public abstract void buildControl();

public TechWeb construct(){
buildControl();
buildList();
buildMenu();
buildWindow();
return techWeb;
}
}

/**
* 完整模式 - 具体建造者
*/
class FullPattern extends ShowPattern{

@Override
public void buildMenu() {
techWeb.setControl("完整控制条");
}

@Override
public void buildList() {
techWeb.setList("显示50条");
}

@Override
public void buildWindow() {
techWeb.setMenu("菜单列表50条");
}

@Override
public void buildControl() {
techWeb.setWindow("带所有菜单的窗口");
}
}

/**
* 精简模式
*/
class SimpPattern extends ShowPattern{

@Override
public void buildMenu() {
techWeb.setControl("完整的控制条");
}

@Override
public void buildList() {
techWeb.setList("不显示播放列表");
}

@Override
public void buildWindow() {
techWeb.setMenu("不显示菜单");
}

@Override
public void buildControl() {
techWeb.setWindow("不带菜单的窗口");
}
}

class Client {
public static void main(String[] args) {
ShowPattern simpPattern = new SimpPattern();
TechWeb web = simpPattern.construct();
System.out.println(web.getMenu());
}
}

第七章 适配器模式 - 不兼容结构的协调

一、概念

如笔记本的电源接口只支持20V电压,而家庭用电的电压为220V,肯定无法直接通过一根电线直接为笔记本提供电源。此时,就需要一个变压器电源,来将220V电压转换为20V电压。这个变压器就是适配器模式的一种。

适配器模式:将一个类的接口与另一个类的接口匹配利用,而无须修改原来的适配者,解耦合抽象目标类接口。

适配器模式可以是类结构型模式,也可以是对象结构型模式。

1.2 适配器模式中的几个角色

  1. Target - 目标抽象类
    1. 目标抽象类定义客户所需接口
    2. 抽象类、接口或具体类
    3. 类似上面的笔记本
  2. Adapter - 适配器类
    1. 适配器可以调用另一个接口,作为一个转换器,对 Adaptee 和 Target 进行适配
  3. Adaptee - 适配者类
    1. 被适配的角色(如220V的家庭电源,我们需要将其转换为20V电压输出)
    2. 它定义了一个已经存在的接口,这个接口需要适配
    3. 适配者类一般是个具体类,包含了客户希望使用的业务方法

1.3 对象适配器 - 实例

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
// 抽象成绩操作类 - Target目标接口
interface ScoreOperation {
public int[] sort(int array[]); // 成绩排序
public int search(int array[],int key); // 成绩查找
}

// 快速排序类 - Adaptee 适配者类
class QuickSort{
public int[] quickSort(int array[]){
// 自己实现
}
}

// 二分查找类 - Adaptee 适配者类
class BinarySearch{
public int binarySearch(int array[],intkey){
// 自己实现
}
}

// 操作适配器 - Adapter 适配器
class OperationAdapter implements ScoreOperation{
private QuickSort sort = new QuickSort;
private BinarySearch search = new BinarySearch();

// 调用适配者类QuickSort的排序方法
public int[] sort(int array[]){
return sort.quickSort(array);
}

// 调用适配者类BinarySearch的查找方法
public int search(int array[],int key){
return search.binarySearch(array,key);
}
}

1.4 类适配器

类适配器与对象适配器的区别在于:

对象适配器与适配者之间的关系是关联关系,而类适配器中的关系则是继承关系

如下:

1
2
3
4
5
class Adapter extends Adaptee implements Target{
public void request(){
specificRequest();
}
}

由于Java、C#等语言不支持多重继承,因此类适配器在使用时受到诸多限制。如Target必须是接口,Adapter不能是Final类等等。

1.5 双向适配器

在对象适配器的使用过程中,如果在适配器中同时包含对目标类和适配者类的引用,适配者和目标类可以通过它调用彼此的方法,那么该适配器就是一个双向适配器。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Adapter implements Adaptee,Target{
private Target target;
private Adaptee adaptee;

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

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

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

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

1.6 缺省适配器

缺省适配器模式(Default Adapter Pattern):当不需要实现一个接口所提供的所有方法时,可先设计一个抽象类实现该接口,并为接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可以选择性地覆盖父类的某些方法来实现需求,它适用于不想使用一个接口中的所有方法的情况,又称为单接口适配器模式。

二、总结

优点

  1. 目标类与适配者类解耦,通过引入一个适配器类来重用现有的适配者类
  2. 增加类的透明性和复用性,具体业务过程封装在适配者类中,对客户端透明,提供了适配者的复用性,同一个适配者类可以在多个不同系统中复用
  3. 灵活性和扩展性好,可以通过配置文件的方式更换适配器,符合“开闭原则”

缺点

  1. Java、C#不支持多重继承,一次只能适配一个适配者类
  2. 适配者类不能为最终类
  3. Java、C#中,类适配器模式的目标抽象类必须是接口

适用场景

  1. 系统需要使用一些现有的类,但该类的接口不符合系统需要
  2. 创建一个可重复使用的类,如上面的快速排序,可以做成一个适配者,为其它适配器类所调用

三、练习

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
58
59
60
61
62
63
64
65
66
67
// SD卡接口及其具体实现
public interface SDCard {
String readSD(); // 读SD卡
int writeSD(String message); // 写SD卡
}
class SDCardImpl implements SDCard {
public String readSD() {
return "读取SD卡";
}
public int writeSD(String message) {
// 假装写入数据至SD卡中了
return 0;
}
}

// 电脑类,具有读取SD卡的功能
class Computer {
String readSD(SDCard sdCard) {
return sdCard.readSD();
}
}

// ----- 新版的TF卡上市,系统需要在不修改原有SD卡接口的情况下,支持新版的TF卡 -----

// TF卡接口及其实现
interface TFCard {
String readTF(); // 读TF卡
int writeTF(String message); // 写TF卡
}
class TFCardImpl implements TFCard {
public String readTF() {
return "读取TF卡";
}
public int writeTF(String message) {
return 0;
}
}

// TF卡的适配器,继承与目标接口,但是将SD卡接口适配为TF卡的接口
class TFCardAdapter implements SDCard {
private TFCard tfCard;
public TFCardAdapter(TFCard tfCard) {
this.tfCard = tfCard;
}
@Override
public String readSD() {
return tfCard.readTF();
}
@Override
public int writeSD(String message) {
return tfCard.writeTF(message);
}
}

// 测试客户端
class Client {
public static void main(String[] args) {
SDCard sdCard = new SDCardImpl();
SDCard tfCard = new TFCardAdapter(new TFCardImpl()); // 通过适配器,将TFCard适配为SDCard

Computer computer = new Computer();
String s = computer.readSD(sdCard);
String s1 = computer.readSD(tfCard);
System.out.println(s);
System.out.println(s1);
}
}

适配器类的名称尽量做到见名知意,因为客户端表面上调用的是SDCard,但是内部却被适配成了TFCard,如果系统中存在大量这种代码,会使系统十分的混乱。

如果适配器大量存在的话,可以考虑重构代码,而不是继续适配下去。


第八章 桥接模式 - 处理多维度变化

一、基本概念

1.1 场景

有两种文具,分别是毛笔和蜡笔。如果需要大中小三种型号的蜡笔,且每种型号需要有12种不同的颜色。

则蜡笔需要各种型号的各种颜色 3*12 = 36支,而毛笔只需要大中小型号和12中颜色的颜料盒 3 + 12 = 15支。

在蜡笔的模式中,无论是增加颜色,还是增加型号,都会影响另一个维度。耦合度非常的强。

但在毛笔中,型号与颜色进行了解耦,只是在使用时组合使用,非常灵活,扩展也十分方便。

1.2 场景二 - 跨平台图像浏览系统

现有一个系统,需要该系统支持BMP、JPG、GIF等格式的图形文件,并且能在Windows、Linux、MacOS等多个系统上运行。首先将文件解析为像素矩阵,然后各系统使用各自的绘制函数,将矩阵显示在屏幕上。

如果使用上面的“蜡笔模式” - 多层继承模式,可能会存在 Image -> BMPImage、JPGImage、GIFIMAGE -> BMPWindowsImage/BMPLinuxImage/BMPMacOSImage、JPGWindowsImage/JPGLinuxImage/JPGMacOSImage、GIFWindowsImage/GIFLinuxImage/GIFMacOSImage。

存在的问题:

  1. 由于采用了多层继承,系统中的类个数非常多。具体类的个数 = 所支持的图像格式 * 所支持的操作系统
  2. 系统扩展麻烦,如BMPWindowsImage,即包含图像文件格式,又包含操作系统信息。无论是增加图像格式还是操作系统,都需要增加大量具体类。
  3. 违反了“单一职责原则”,因为具体类将图像文件解析和像素矩阵显示这两个完全不同的职责融合,任意职责发生改变,都需要修改所有。

解决方案:

引入桥接模式,将两个独立变化的维度设计为两个独立的继承等级结构,并且在抽象层建立一个抽象关联。

二、桥接模式

2.1 定义

桥接模式,又称为柄体模式或接口模式:将抽象部分与它的实现部分分离,使它们都可以独立地变化。是一种非常实用的结构性设计模式。

如果软件系统中某个类存在两个独立变化的维度,通过该模式可以将这两个维度分离出来,使两者可以独立扩展,让系统更加符合“单一职责原则”。

桥接模式用一种巧妙的方式处理多层继承存在的问题,用抽象关联取代传统的多层继承,将类之间的静态继承关系转换为动态的对象组合关系,使系统更加灵活,更易于扩展,同时有效控制了系统中类的个数。

桥接模式是一个非常有用的模式,包含了很多面向对象设计原则的思想,如“单一职责原则”、“开闭原则”、“合成复用原则”、“里氏替换原则”、“依赖倒转原则”等。有助于我们深入理解设计原则,形成正确的设计思想和培养良好的设计风格。

2.2 桥接模式中的几个角色

  1. Abstraction - 抽象类
    1. 用于定义抽象类的接口,他一般是抽象类而不是接口
    2. 定义了一个 Implementor实现类接口 类型的对象,并可以维护该对象,它与实现类接口之间具有关联关系
    3. 既可以包含抽象业务方法,也可以包含具体业务方法
  2. RefinedAbstraction - 扩充抽象类
    1. 一般为具体类,实现了 Abstraction抽象类 中的抽象方法
    2. 因为 Abstraction抽象类 中定义了实现类接口对象,所以RefinedAbstraction扩充抽象类中可以调用 Implementor具体实现了类 中定义的业务方法
  3. Implementor - 实现类接口
    1. 定义实现类接口,提供基本操作
    2. 具体实现交给其子类
    3. 通过关联关系,在 Abstraction抽象类 中,可以直接调用 Implementor及其子类 的方法
    4. 使用关联关系来代替继承关系
  4. ConcreteImplementor - 具体实现类
    1. 实现 Implementor接口,不同的具体实现类提供不同的方法实现
    2. 用于提供给 Abstraction抽象类 调用的具体实现

2.3 具体实现

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
58
59
60
61
62
63
64
65
66
67
68
69
// 桥接模式
public class BridgePattern {

// 像素矩阵类:辅助类,各种格式的文件最终都被转化为像素矩阵,不同的操作系统提供不同的方式显示像素矩阵
static class Matrix {

}

// 抽象图像类:抽象类
static abstract class Image {
protected ImageImp imp;

public void setImp(ImageImp imp) {
this.imp = imp;
}

public abstract void parseFile(String fileName);
}

// JPG格式图像:扩充抽象类
static class JPGImage extends Image {
@Override
public void parseFile(String fileName) {
final Matrix matrix = new Matrix();
imp.doPaint(matrix);
System.out.println(fileName + ",格式为JPG");
}
}

// PNG格式图像:扩充抽象类
static class PNGImage extends Image {
@Override
public void parseFile(String fileName) {
final Matrix matrix = new Matrix();
imp.doPaint(matrix);
System.out.println(fileName + ",格式为PNG");
}
}

// 抽象操作系统实现类:实现类接口
static interface ImageImp {
public void doPaint(Matrix m); // 显示像素矩阵 m
}

// Windows 操作系统实现类:具体实现类
static class WindowsImp implements ImageImp {
@Override
public void doPaint(Matrix m) {
// 调用Windows系统的绘制函数,绘制像素矩阵
System.out.println("在Windows操作系统中显示图像");
}
}

// Linux 操作系统实现类:具体实现类
static class LinuxImp implements ImageImp {
@Override
public void doPaint(Matrix m) {
// 调用Linux系统的绘制函数,绘制像素矩阵
System.out.println("在Linux操作系统中显示图像");
}
}

public static void main(String[] args) {
Image img = new PNGImage();
ImageImp imp = new WindowsImp();
img.setImp(imp);
img.parseFile("死了都要爱.jpg");
}
}

三、总结

在使用桥接模式时,我们应该先识别出一个类所具有的两个独立变化的维度,将它们设计为两个独立的继承等级结构,为两个维度都提供抽象层,并建立抽象耦合。通常情况下,我们将具有两个独立变化维度的类的一些普通业务方法,和与之关系最密切的维度设计为“抽象类”层次结构(抽象部分),而将另一个维度设计为“实现类”层次结构(实现部分)。

如毛笔,由于型号是固有的维度,可以设计为抽象毛笔类,在该类中声明并部分实现毛笔的业务方法,将各种型号的毛笔作为子类。将颜色作为毛笔的另一个维度。

增加新的毛笔类型,只需扩展左侧的“抽象部分”;增加新的颜色,只需扩展右侧的“实现部分”。

优点

  1. 分离抽象解耦及其实现部分。解耦了抽象和实现之间的绑定关系,使抽象部分和实现部分都有自己的维度变化
  2. 很多情况下,桥接模式可以取代多层继承方案。多层继承违背了“单一职责原则”,复用性较差,且类的个数较多
  3. 桥接模式提供了系统的可扩展性,两个维度的扩展都不需要修改原有系统,符合“开闭原则”

缺点

  1. 桥接模式的使用,会增加系统的理解和设计难度,由于关联关系建立在抽象层,要求开发者一开始就针对抽象层进行设计和编程
  2. 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性。

适用场景

  1. 如果一个系统需要在抽象化和具体化之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系,通过桥接模式可以使它们在抽象层建立一个关联关系
  2. “抽象部分”和“实现部分”可以以继承的方式独立扩展而不受影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合
  3. 一个类存在两个(或多个)独立变化的维度,且各维度都需要独立进行扩展
  4. 对于不希望使用继承或多层继承的系统,导致系统类的个数急剧增加的系统,桥接模式非常适用

四、扩展

开发一个数据转换工具,可以将数据库中的数据转换成多种文件格式,例如txt、xml、pdf等格式,同时该工具需要支持多种不同的数据库。试使用桥接模式对其进行设计。

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
58
59
60
// 桥接模式
public class BridgePattern {

// 数据库抽象类
static abstract class Database {
protected FileFormat fileFormat;

public void setFileFormat(FileFormat fileFormat) {
this.fileFormat = fileFormat;
}

public abstract void transition();
}

// Oracle 数据库 - 抽象扩展类
static class OracleDatabase extends Database {
@Override
public void transition() {
System.out.println("Oracle数据库");
fileFormat.file();
}
}

// Microsoft 数据库 - 抽象扩展类
static class MCDatabase extends Database {
@Override
public void transition() {
System.out.println("Microsoft数据库");
fileFormat.file();
}
}

// 文件格式 - 实现类接口
interface FileFormat {
void file();
}

// txt 文件 - 具体实现类
static class TxtFileFormat implements FileFormat {
@Override
public void file() {
System.out.println("TXT 格式文本输出");
}
}

// pdf 文件 - 具体实现类
static class PDFFileFormat implements FileFormat {
@Override
public void file() {
System.out.println("PDF 格式文本输出");
}
}

public static void main(String[] args) {
Database db = new OracleDatabase();
FileFormat ff = new TxtFileFormat();
db.setFileFormat(ff);
db.transition();
}
}

第九章 组合模式 - 树形结构的处理

一、杀毒软件的框架结构

1. 介绍

开发一个杀毒软件,既可以对某个文件夹(Folder)杀毒,也可以对指定文件(File)进行杀毒。

2. 面向对象的解法

按照面向对象的设计思路,应该在 Folder 中包含图像、文本等文件类型的集合,以及文件夹的集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ImageFile {
// image property
}

class TextFile {
// text property
}

class Folder {
List<ImageFile> images = new ArrayList<>();
List<TextFile> texts = new ArrayList<>();
List<Folder> folders = new ArrayList<>();

void addFolder();
void addImage();
void addText();

// ... remove/find etc.
}

3. 存在的问题

  1. Folder 的设计和实现十分复杂,需要定义多个集合存储不同类型的成员,且需要针对不同成员提供各自的 增删改查方法,存在大量冗余代码,系统维护困难
  2. 由于系统没有抽象层,客户端必须有区别地对待 文件夹和各类型的文件,无法对其进行统一处理
  3. 系统灵活性和可扩展性差,增加新类型的叶子和容器都需要对原有代码进行修改

4. 解决思路

整个架构中包含两类不同的元素,文件夹和文件。文件夹中可以包含文件或文件夹、而文件不能再包含子文件或子文件夹。因此,我们可以将文件夹成为 容器(Container),不同类型的文件是其成员,也称为 叶子(Leaf)。

二、组合模式

1. 定义

对于树形结构,当容器对象(如文件夹)的某一个方法被调用时,将遍历整个属性结构,其中使用了递归调用的机制来对整个结构进行处理。由于容器对象和叶子对象在功能上的区别,在使用时必须有区别地对待。而大多数情况下我们希望一致地处理它们,因为对于这些对象的使用具有一致性。

组合模式:组合多个对象形成树形结构,以表示具有“整体 - 部分” 关系的层次结构。组合模式对单个对象(叶子对象)和组合对象(容器对象)的使用具有一致性,组合模式又可以成为“整体 - 部分”模式,属于对象结构性模式。

2. 组合模式中的几个角色

  1. Component - 抽象构件
    1. 接口或抽象类,为 Leaf 和 Composite 对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现
  2. Leaf - 叶子构件
    1. 叶子节点,没有子节点,实现了在抽象构件中定义的行为
    2. 对于访问及管理子构件的方法,可以通过异常等方式进行处理
  3. Composite - 容器构件
    1. 容器节点对象,包含子节点
    2. 子节点可以是叶子节点,也可以是容器节点
    3. 提供一个集合用于存储子节点,实现了抽象构件中定义的行为,包含那些访问及管理子节点的方法
    4. 在业务方法中可以递归调用子节点的业务方法

三、 完整解决方案

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// 组合模式
abstract class AbstractFile {
public abstract void add(AbstractFile file);

public abstract void remove(AbstractFile file);

public abstract AbstractFile getChild(int i);

public abstract void killVirus();
}

class ImageFile extends AbstractFile {
private String name;

public ImageFile(String name) {
this.name = name;
}

@Override
public void add(AbstractFile file) {
System.out.println("不支持该方法!");
}

@Override
public void remove(AbstractFile file) {
System.out.println("不支持该方法!");
}

@Override
public AbstractFile getChild(int i) {
System.out.println("不支持该方法!");
return null;
}

@Override
public void killVirus() {
System.out.println("对图像进行[" + name + "]杀毒");
}
}

class TextFile extends AbstractFile {
private String name;

public TextFile(String name) {
this.name = name;
}

@Override
public void add(AbstractFile file) {
System.out.println("不支持该方法!");
}

@Override
public void remove(AbstractFile file) {
System.out.println("不支持该方法!");
}

@Override
public AbstractFile getChild(int i) {
System.out.println("不支持该方法!");
return null;
}

@Override
public void killVirus() {
System.out.println("对文本进行[" + name + "]杀毒");
}
}

class Folder extends AbstractFile {
private List<AbstractFile> files = new ArrayList<>();
private String name;

public Folder(String name) {
this.name = name;
}

@Override
public void add(AbstractFile file) {
files.add(file);
}

@Override
public void remove(AbstractFile file) {
files.remove(file);
}

@Override
public AbstractFile getChild(int i) {
return files.get(i);
}

@Override
public void killVirus() {
System.out.println("对文件夹[" + name + "]进行杀毒");
for (AbstractFile file : files) {
file.killVirus();
}
}
}

public class CompositePattern {
public static void main(String[] args) {
AbstractFile folder1 = new Folder("图像文件夹");
AbstractFile folder2 = new Folder("文本文件夹");

AbstractFile file1 = new ImageFile("图片一");
AbstractFile file2 = new ImageFile("图片二");

AbstractFile file3 = new ImageFile("文本一");
AbstractFile file4 = new ImageFile("文本二");

folder1.add(file1);
folder1.add(file2);

folder2.add(file3);
folder2.add(file4);
folder2.add(folder1);

folder2.killVirus();
}
}

四、透明组合模式和安全组合模式

通过引入 组合模式,该杀毒软件具有良好的可扩展性,在新增文件类型时,只需要新增一个文件类继承 AbstractFile 即可。

但是 Abstract 中声明了大量用于管理和访问成员构件的方法,如 add() remove() 等方法,提供对应的错误提示和异常处理。

解决方案一:将叶子构件的add()、remove() 方法移至AbstractFile中,提供默认实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
abstract class AbstractFile {
public void add(AbstractFile file){
System.out.println("对不起,不支持该方法!")
}
public void remove(AbstractFile file){
System.out.println("对不起,不支持该方法!")
}
public AbstractFile getChild(int i){
System.out.println("对不起,不支持该方法!")
}

public abstract void killVirus();
}

解决方案二:不提供 add()、remove() 等抽象方法,由具体需要的子构件自己提供。但是会导致客户端不得不使用容器类本身来声明容器构件对象,否则无法访问其中新增的方法。客户端代码无法通过容器构件的抽象构件来定义。

1. 透明组合模式

上面的解决方案一,就是透明组合模式的实现。由抽象构件提供子构件方法的默认实现。

好处是确保所有构件类都有相同的接口,对客户端来说,叶子对象和容器对象的方法一致。

缺点是,因为叶子对象和容器对象在本质上是有区别的,叶子对象不可能有下一层对象,因此不应该有 add()/remove() 等方法,在运行时可能会出现异常。

2. 安全组合模式

解决方案二,就是安全组合模式的实现。抽象构件中不声明管理成员对象的方法,而是在 Composite类中声明并实现这些方法。

这种做法是安全的,因为叶子对象没有了管理成员对象的方法,客户端也就不能对叶子对象调用这些方法了。

五、总结

组合模式使用面向对象的思想来实现树形结构的构建与处理,描述了如何将容器对象和叶子对象进行递归组合,实现简单,灵活型号。

优点

  1. 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,使客户端忽略了层次的差异,方便对整个层次结构进行控制
  2. 客户端可以一致地使用一个组合结构或其中的单个对象,不必关系处理的是单个对象还是整个组合结构,简化客户端代码
  3. 组合模式为树形结构的面向对象实现,提供了一种灵活的解决方案,通过叶子对象和容器对象的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单

缺点

  1. 增加新构件时很难对容器中的构件类型进行限制

适用场景

  1. 在具有整合和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致地对待它们
  2. 在一种使用面向对象语言开发的系统中需要处理一个树形结构
  3. 在一个系统中能够分离出叶子对象和容器对象,而且它们的类型不固定,需要增加一些新的类型

六、练习

Sunny软件公司欲开发一个界面控件库,界面控件分为两大类,一类是单元控件,例如按钮、文本框等,一类是容器控件,例如窗体、中间面板等,试用组合模式设计该界面控件库。

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/**
* 组合模式:
* Sunny软件公司欲开发一个界面控件库,
* 界面控件分为两大类,一类是单元控件,例如按钮、文本框等,
* 一类是容器控件,例如窗体、中间面板等,
* 试用组合模式设计该界面控件库。
*/
abstract class UI {
abstract void display();

void add(UI ui){
System.out.println("不支持该方法");
}
}

class ButtonUI extends UI {
@Override
void display() {
System.out.println("显示按钮");
}
}

class InputUI extends UI {
@Override
void display() {
System.out.println("显示输入框");
}
}

class Window extends UI{
List<UI> uis = new ArrayList<>();

@Override
void display() {
System.out.println("--开始显示窗口--");
for (UI ui : uis) {
ui.display();
}
}

@Override
void add(UI ui) {
uis.add(ui);
}
}

class Dashboard extends UI {
List<UI> uis = new ArrayList<>();

@Override
void display() {
System.out.println("--开始显示面板--");
for (UI ui : uis) {
ui.display();
}
}

@Override
void add(UI ui) {
uis.add(ui);
}
}

public class CompositePattern {
public static void main(String[] args) {
final UI ui1 = new ButtonUI();
final UI ui2 = new ButtonUI();
final UI ui3 = new InputUI();
final UI ui4 = new InputUI();

final UI window = new Window();
final UI dashboard = new Dashboard();

dashboard.add(ui1);
dashboard.add(ui4);
window.add(ui1);
window.add(ui3);
window.add(ui2);
window.add(dashboard);

window.display();
}
}


第十章 装饰模式 - 扩展系统功能

如果你买了一个毛坯房,那么剩下的就是装修。装修并没有改变原有房屋用于居住的本质,但是增加了实用性、美观性等特征。

在软件设计中,装饰模式就是一种类似装修的技术,能对已有对象的功能进行扩展,以获得更符合用户需求的对象。

一、设计一款图形界面库

设计一款图形界面构件库,该库提供了大量基础构件,如窗体、文本框、列表框等。由于在使用时,需要定制一些特效,如带滚动条的窗体、带黑色边框的文本框、即带滚动条又带黑色边框的列表框等。即对原有的基础构件进行扩展,以增强其功能。

1.1 传统继承方式实现

传统继承的方式实现

1.2 问题与缺陷

按照 1.1 中的实现方式,虽然可以满足系统的设计需求。但是,存在的问题也十分严重:

  1. 系统扩展十分麻烦。当我们需要 “带滚动条和黑色边框的窗体类” 时,需要同时继承两种类型的窗体类。这在 不支持多重继承 的语言中是无法使用的
  2. 代码重复。从设计图中可以看出,不只是 窗体类 需要滚动和边框,文本框类和列表框类同样需要。而设计滚动条与黑色边框的过程基本相同,代码重复。不利于对系统进行修改和维护。
  3. 系统庞大,类的数目非常多。如果增加新的控件或者新的扩展功能,系统都需要增加大量的具体类,这将导致系统变得非常庞大。
    1. 如增加一个透明边框(基本控件)的功能,则需要对窗体、文本框、列表框各加一个实现类。
    2. 如果需要组合各个功能的话,3种扩展方式则存在7种组合关系。

1.3 解决方案

直接继承的设计方法的问题在于 类的扩展十分不便,而且会导致类数目的急剧增加。

其根本原因在于复用机制的不合理:上文采用了继承复用,如“带滚动的窗体”通过继承的方式来复用“窗体类”的“显示功能”,又增加了特定的方法。在复用父类的方法后再增加新的方法来扩展功能。

根据“合成复用原则”,在实现功能复用时,要多用关联,少用继承。如将 “滚动功能” 抽离,封装在单独的类中,在这个类中定义一个 Component 类型的对象,通过调用 Comonent 的 “显示方法”,在通过调用“滚动功能”的方法来实现功能的扩展。

根据“里氏替换原则”,只需要在程序运行时,向独立的类中注入具体的 Component 子类对象即可实现功能扩展。

这个独立的类一般称为“装饰器 Decorator” 或装饰类。

二、装饰模式

装饰模式可以在不改变一个对象本身功能的基础上给对象增加额外的新行为。

装饰模式是一种用于替代继承的技术,它通过一种无须定义子类的方式来给对象动态增加职责,使用对象之间的关联关系取代类之间的继承关系。通过装饰类调用待装饰的原有类的方法,还可以增加新的方法,以扩充原有类的功能。

2.1 定义

装饰模式:动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更加灵活。装饰模式是一种对象结构型模式。

装饰模式

2.2 装饰模式中的几个角色

  1. Component - 抽象构件
    1. 具体构件和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法
    2. 引入抽象构件,可以使客户端以一致的方式处理未被装饰的对象,以及装饰之后的对象
    3. 实现客户端的透明操作
  2. ConcreteComponent - 具体构件
    1. 抽象构件的子类,用于定义具体的构件对象
    2. 装饰器可以给它增加额外的职责(方法)
  3. Decorator - 抽象装饰类
    1. 抽象构件的子类,用于给具体构件增加职责,但是具体职责在其子类中实现
    2. 维护一个指向抽象构件对象的引用,通过该引用可以调用装饰之前构件对象的方法
    3. 通过子类扩展该方法,以达到装饰的目的
  4. ConcreteDecorator - 具体装饰类
    1. 抽象装饰类的子类,负责向构件添加新的职责
    2. 每个具体装饰类都定义了一些新的行为,可以调用抽象装饰类中定义的方法,并增加新的方法用以扩充对象的行为

2.3 装饰模式的核心 - 抽象装饰类 - 代码

1
2
3
4
5
6
7
8
9
10
class Decorator implements Component {
private Component component; // 维护一个对抽象构件对象的引用
public Decorator(Component component) { // 注入一个抽象构件类型的对象
this.component = component;
}

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

抽象装饰类可以做到对装饰类进行再装饰,如对一张图表增加一个相框,还能继续在小相框外套大相框。因为它们都是 Component 的子类。

抽象装饰类只是调用原有的 component 对象的 operation() 方法,它并没有真正的实施装饰,而是提供一个统一的接口,将具体装饰过程交给子类完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ConcreteDecorator extends Decorator {
public ConcreteDecorator(Component component){
super(component);
}

public void operation(){
super.operation(); // 调用原有业务方法
addedBehavior(); // 调用新增业务方法
}

// 新增的业务方法
public void addedBehavior(){
// ...
}
}

装饰模式中是否存在独立变化的两个维度? 试比较装饰模式和桥接模式的相同之处和不同之处?

三、完整实现

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/**
* 装饰模式
*/
// 抽象界面构件类 - 抽象构件类
abstract class Component {
public abstract void display();
}

// 窗体类 - 具体构件类
class Window extends Component {
@Override
public void display() {
System.out.println("显示窗体!");
}
}

// 文本框类 - 具体构件类
class TextBox extends Component {
@Override
public void display() {
System.out.println("显示文本框!");
}
}

// 列表框类 - 具体构件类
class ListBox extends Component {
@Override
public void display() {
System.out.println("显示列表框!");
}
}

// 构件装饰类 - 抽象装饰类
class ComponentDecorator extends Component {
// 维持对抽象构件类型对象的引用
private Component component;

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

@Override
public void display() {
component.display();
}
}

// 滚动条装饰类 - 具体装饰类
class ScrollBarDecorator extends ComponentDecorator {

public ScrollBarDecorator(Component component) {
super(component);
}

@Override
public void display() {
this.setScrollBar();
super.display();
}

public void setScrollBar() {
System.out.println("为构件增加滚动条!");
}
}

// 黑色边框装饰类 - 具体装饰类
class BlackBorderDecorator extends ComponentDecorator {
public BlackBorderDecorator(Component component) {
super(component);
}

@Override
public void display() {
setBlackBorder();
super.display();
}

public void setBlackBorder() {
System.out.println("为构件增加黑色边框");
}
}

public class DecoratorPattern {

public static void main(String[] args) {
Component window = new Window();
Component scrollBarWindow = new ScrollBarDecorator(window);
scrollBarWindow.display();
}
}

四、透明装饰模式和半透明装饰模式

如果我们需要在 “黑色边框装饰类” 中增加一个方法用于设置边框宽度。则修改后的类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 黑色边框装饰类 - 具体装饰类
class BlackBorderDecorator extends ComponentDecorator {
public BlackBorderDecorator(Component component) {
super(component);
}

@Override
public void display() {
setBlackBorder();
super.display();
}

public void setBlackBorder() {
System.out.println("为构件增加黑色边框");
}

public void setBorderWidth(Integer width){
// ...
}
}

如果我们继续使用 抽象构件类 ,则客户端无法调用新增的业务方法 setBorderWidth(Integer width)。因为在抽象构建类中,没有对该方法的声明。

在实际使用中,必须使用具体装饰类对象来调用该方法,这种装饰模式就是 半透明(Semi-transparent)装饰模式,而标准的装饰模式是透明装饰模式。

透明装饰模式与半透明装饰模式的区别

透明装饰模式要求客户端完全针对抽象编程,所有的装饰类必须声明为抽象构件类型。

  1. 优点:客户端无需关心具体构件类型,可以让客户端透明地使用装饰之前的对象和装饰之后的对象,无须关系它们的区别
  2. 优点:能够对装饰过的对象进行多次装饰,得到更复杂、功能更强大的对象。

而半透明装饰模式的设计难度较大,对于新增特有的方法,必须使用具体装饰类型来定义装饰后的对象。

  1. 优点:半透明装饰模式可以给系统带来更多灵活性,设计相对简单,使用也比较方便
  2. 缺点:不能对使用同一对象的多次装饰,且客户端需要区别对待装饰之前和装饰之后的对象

五、总结

优点

  1. 对于扩展对象的功能,装饰模式比继承更加灵活,且类的个数不会急剧增加
  2. 能够动态扩展对象功能,比如通过配置文件决定运行时的具体装饰类
  3. 能够对一个对象进行多次装饰,通过使用不同的具体装饰类来获得不同行为的组合
  4. 具体构件类和具体装饰类可以独立变化,符合“开闭原则”

缺点

  1. 装饰模式在使用时,会产生许多的对象。
    1. 例如通过对一个对象的多次包装,会产生多个具体包装类
    2. 大量对象势必会占用更多的系统资源,一定程度上影响程序的性能
  2. 装饰模式虽然比继承更加灵活,但也意味着更加容易出错,排查问题更加困难。
    1. 对于多次装饰后的对象,调试问题可能需要逐级排查,较为繁琐

适用场景

  1. 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责
  2. 不能采用继承方式对系统扩展时。
    1. 系统中存在大量独立的扩展,增加扩展时会导致子类爆炸性增长
    2. 类被定义为不能继承(如final修饰的类)

六、练习

Sunny软件公司欲开发了一个数据加密模块,可以对字符串进行加密。最简单的加密算法通过对字母进行移位来实现,同时还提供了稍复杂的逆向输出加密,还提供了更为高级的求模加密。用户先使用最简单的加密算法对字符串进行加密,如果觉得还不够可以对加密之后的结果使用其他加密算法进行二次加密,当然也可以进行第三次加密。试使用装饰模式设计该多重加密系统。


第十一章 外观模式

一、外观模式

在日常开发中,注册一个用户需要进行如下操作:验证手机验证码、生成用户类、存储至数据库、初始化用户积分等等操作。但是 客户端只需要调用一个 UserService.register() 即可,具体的其他对象的操作都交给 UserService 的实现来完成。

还有如 MVC 模式中,Controller 层中通过调用 service 来解耦 Controller 层与 Dao 层,隔离各个层次,实现层次化结构。

于 controller 而言,service 就是外观类,由service 来处理多个dao的交互。

1.1 定义

外观模式,又称为门面模式,是一种使用频率非常高的结构型设计模式,通过引入一个外观对象来简化客户端与子系统之间的交互,为复杂的子系统调用提供一个统一的入口,降低子系统与客户端的耦合度,且客户端调用非常方便。

外观模式是迪米特法则的一种具体实现,通过引入一个新的外观角色可以降低原油系统的复杂度,同时降低客户类与子系统的耦合。

1.2 外观模式

外观模式示意图

在日常生活中,自己泡茶和去茶馆喝茶的区别类似于上图显示的这样。自己泡需要与 热水、差距、茶叶 进行交互(系统耦合度十分的高),而去茶馆喝茶只需要与服务员(Facade)进行交互即可。

1.3 外观模式中的几个角色:

  1. Facade - 外观角色
    1. 由客户端调用其中的方法,来访问相关子系统的功能
    2. 由外观角色将客户端的请求委派到具体子系统中
  2. SubSystem - 子系统角色
    1. 系统中可以存在一个或多个子系统角色
    2. 每个可以被客户端调用,也可以被外观角色调用,用于处理请求
    3. 子系统并不知道外观的存在,与它而言,外观角色仅仅是另一个客户端

二、完整实现

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
/**
* 外观模式
*/
class SubSystemA {
public void sayHello() {
System.out.println("大家好,我是系统A");
}
}

class SubSystemB {
public void byebye() {
System.out.println("再见,我是系统B");
}
}

class SubSystemC {
public void o() {
System.out.println("i don't wanna see you anymore.");
}
}

class Facade {
private SubSystemA a = new SubSystemA();
private SubSystemB b = new SubSystemB();
private SubSystemC c = new SubSystemC();

public void bb(){
a.sayHello();
c.o();
b.byebye();
}
}

public class FacadePattern {
public static void main(String[] args) {
// 通过外观类 Facade 减少了客户端与各个子系统的直接交互,降低耦合度
final Facade facade = new Facade();
facade.bb();
}
}

三、抽象外观类

在标准外观模式中,如果需要增加、删除或更换与外观类交互的子系统类,必须修改外观类或客户端的代码。这将违背开闭原则。

可以引入抽象外观类来对系统进行改进,客户端针对抽象外观类进行编程。

四、总结

优点

  1. 对客户端屏蔽子系统组件,减少客户端所需处理的对象数目,使子系统的使用更加容易
  2. 客户端与子系统间关联的对象会减少
  3. 实现了子系统和客户端之间的松耦合,子系统的变化只需要调整外观类即可
  4. 子系统的修改不会影响其他子系统,且子系统内部修改不会影响到外观类

缺点

  1. 由外观类来调用子系统类,减少了客户端直接调用时的灵活性和可变性
  2. 如果设计不当,增加子系统时可能会修改外观类,违背了开闭原则

适用场景

  1. 当需要为访问一系列复杂的子系统对象提供简单入口时,可以使用外观模式
  2. 外观类可以将子系统与客户端解耦,从而提高子系统的独立性和可移植性
  3. 在层次化结构中,可以使用外观模式定义系统中的每一层入口。层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度

第十二章 享元模式

在Java开发或者面试过程中,经常会遇到一个面试题:

1
2
3
String a = new String("abc"); 
String b = new String("abc");
问:a == b ?

答案是肯定的,因为JVM在创建一个字符串后,会将其存储在 字符串池 中,下次new时会先去 字符串池 中查询是否已经存在。然后将引用地址返回。

这就是一个典型的 享元模式 案例。

一、设计一个围棋软件

对于围棋软件而言,棋盘中包含大量的黑白色棋子,它们的形状、大小一致,但是出现的位置不同。如果将所有棋子都 new 一个对象存储在内存中,则该软件在运行时所需的内存空间会非常大。

解决方案

享元模式通过共享技术实现相同或相似对象的重用,在逻辑上每一个出现的字符都有一个对象与之对应,然而在物理上它们却共享同一个享元对象。

在享元模式中,存储这些共享实例对象的地方称为“享元池(Flyweight Pool)”

二、享元模式

享元模式以共享方式,高效地支持大量细粒度对象的重用,享元对象能做到共享的关键是区分了 内部状态(Intrinsic State)和外部状态(Extrinsic State)。

内部状态

内部状态是指存储在享元对象内部并且不会随环境变化而改变的状态,内部状态可以共享,但是不会被修改。

外部状态

外部状态是随环境变化而变化的、不可以共享的状态。

外部状态通常由客户端保存,并在享元对象被创建之后,需要使用的时候再传入享元对象内部。

享元模式的定义

享元模式(Flyweight Pattern):运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。

享元模式因其共享的是细粒度对象,因此又称为 轻量级模式,属于对象结构型模式

享元模式中的几个角色

  1. Flyweight - 抽象享元类
    1. 接口或者抽象类,声明了具体享元类的公共方法
    2. 这些方法可以向外界提供内部数据
    3. 也可以有外界来设计外部数据
  2. ConcreteFlyweight - 具体享元类
    1. 实现了抽象享元类,实例被称为享元对象
    2. 具体享元类中为内部状态提供了存储空间
    3. 通常使用单例模式来设计具体享元类
  3. UnsharedConcreteFlyweight - 非共享具体享元类
    1. 不能被共享的子类可以设计为 非共享具体享元类
    2. 在使用时可以直接通过实例化创建
  4. FlyweightFactory - 享元工厂类
    1. 用于创建并管理享元对象,针对抽象享元类编程
    2. 存储享元池,一般设计为“键值对”的集合
    3. 可以结合工厂模式进行设计,返回唯一的实例

三、享元模式的核心

1. 享元工厂类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class FlyweightFactory {
// 定义享元池
private Map<String,Flyweight> flyweights = new HashMap();

public Flyweight getFlyweight(String key){
// 对象存在,则直接从享元池中取
if(flyweights.containsKey(key)){
return flyweights.get(key);
}
// 不存在,则创建一个享元对象,并放入享元池中返回
else {
Flyweight fy = new ConcreteFlyweight();
flyweights.put(key,fy);
return fy;
}
}
}

2. 享元类

享元类将内部状态和外部状态分开处理,通常将内部状态作为享元类的成员变量,而外部状态通过注入的方式添加到享元模式中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Flyweight {
// 内部状态,与享元对象的内部状态一致
private String intrinsicState;

public Flyweight(String intrinsicState){
this.intrinsicState = intrinsicState;
}

// 外部状态,在使用时由外部设置,不保存在享元对象中。
// 即使是同一个享元对象,每次使用时也可以传入不同的外部状态
public void operation(String extrinsicState){
.......
}
}

四、完整实现

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/**
* 享元模式
*/
// 围棋棋子类 - 抽象享元类
abstract class IgoChessman {
// 抽象内部状态
public abstract String getColor();

// 在方法调用时,注入外部状态
public void display(Integer x, Integer y) {
System.out.println("棋子颜色:" + getColor() + ",当前棋子位置为:" + x + "," + y);
}
}

// 黑色棋子类 - 具体享元类
class BlackIgoChessman extends IgoChessman {
@Override
public String getColor() {
return "黑色";
}
}

// 白色棋子类 - 具体享元类
class WhiteIgoChessman extends IgoChessman {
@Override
public String getColor() {
return "白色";
}
}

// 围棋棋子工厂类 - 享元工厂类,使用单例模式进行设计
class IgoChessmanFactory {
private static IgoChessmanFactory instance = new IgoChessmanFactory();
private static Hashtable<String, IgoChessman> ht; // 使用 Hashtable 来存储享元对象,充当享元池

private IgoChessmanFactory() {
ht = new Hashtable<>();
IgoChessman black = new BlackIgoChessman();
IgoChessman white = new WhiteIgoChessman();
ht.put("b", black);
ht.put("w", white);
}

// 返回享元工厂类的唯一实例
public static IgoChessmanFactory getInstance() {
return instance;
}

// 通过key来获取存储在 Hashtable 中的享元对象
public IgoChessman getIgoChessman(String color) {
return ht.get(color);
}
}

public class FlyweightPattern {
public static void main(String[] args) {
// 获取享元工厂对象
IgoChessmanFactory factory = IgoChessmanFactory.getInstance();

// 通过享元工厂获取棋子
IgoChessman black1 = factory.getIgoChessman("b");
IgoChessman black2 = factory.getIgoChessman("b");
IgoChessman black3 = factory.getIgoChessman("b");

IgoChessman white1 = factory.getIgoChessman("w");
IgoChessman white2 = factory.getIgoChessman("w");

black1.display(1,2);
black2.display(2,3);
black3.display(3,4);
white1.display(4,5);
white2.display(2,2);
}
}

五、单纯享元模式与复合享元模式

5.1 单纯享元模式

单纯享元模式中,所有具体享元类都是可以共享的,不存在非共享具体享元类。

5.2 复合享元模式

将一些单纯享元对象使用组合模式进行组合,形成复合享元对象。复合享元对象本身不能共享,但是可以将组合后的享元对象分解为单纯享元对象,进行共享。

六、补充

享元模式与其他模式的联用:

  1. 享元工厂类通常提供一个 静态的工厂方法 用于返回享元对象,使用 简单工厂模式 来生成享元对象
  2. 享元工厂通常是唯一的,可以使用 单例模式 进行设计
  3. 可以使用 组合模式 形成复合享元模式,统一对多个享元对象设置外部状态

七、总结

“节约内存,提高性能”

优点

  1. 极大的减少内存中对象的数量,使得相同或相似对象在内存中只保存一份,从而节约系统资源,提高系统性能
  2. 享元模式的外部状态相对独立,且不影响其内部状态,从而使得享元对象可以在不同环境中被共享

缺点

  1. 享元模式需要分离 内部状态和外部状态,使程序的逻辑更复杂
  2. 为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态将使得运行时间变长

适用场景

  1. 系统中有大量相同或相似的对象,造成大量的内存浪费
  2. 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中
  3. 享元模式在运行时需要维护一个享元池,势必会使内存一直占用,因此享元池中的享元对象需要多次重复使用时才值得

八、练习

Sunny软件公司欲开发一个多功能文档编辑器,在文本文档中可以插入图片、动画、视频等多媒体资料,为了节约系统资源,相同的图片、动画和视频在同一个文档中只需保存一份,但是可以多次重复出现,而且它们每次出现时位置和大小均可不同。试使用享元模式设计该文档编辑器。

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// 抽象享元类 - 声明公共方法,向外界提供内部数据,由外界提供外部数据
abstract class Flyweight {
public abstract String type();

public void insert(String location, String size) {
System.out.println("插入类型:" + type() + ",位置:" + location + ",大小:" + size);
}
}

// 具体享元类 - 实现抽象方法,内部数据的状态与对象状态一致,内部状态不能被修改
class Picture extends Flyweight {
@Override
public String type() {
return "图片类型";
}
}

class Cartoon extends Flyweight {
@Override
public String type() {
return "动画类型";
}
}

class Video extends Flyweight {
@Override
public String type() {
return "视频类型";
}
}

class FlyweightFactory {

// 使用单例模式,维持单一的工厂对象
private static FlyweightFactory factory = new FlyweightFactory();

private Map<String, Flyweight> map = new HashMap<>();

private FlyweightFactory() {
Flyweight picture = new Picture();
Flyweight cartoon = new Cartoon();
Flyweight video = new Video();
map.put("picture", picture);
map.put("cartoon", cartoon);
map.put("video", video);
}

// 静态工厂方法 - 返回工厂对象
public static FlyweightFactory getInstance(){
return factory;
}

public Flyweight getFlyweight(String key) {
if(map.containsKey(key)){
return map.get(key);
}
throw new RuntimeException("flyweight cann't support");
}
}

public class FlyweightPattern {
public static void main(String[] args) {
final FlyweightFactory factory = FlyweightFactory.getInstance();
Flyweight picture1 = factory.getFlyweight("picture");
Flyweight picture2 = factory.getFlyweight("picture");
Flyweight cartoon1 = factory.getFlyweight("cartoon");
Flyweight cartoon2 = factory.getFlyweight("cartoon");
Flyweight video = factory.getFlyweight("video");

picture1.insert("/Pic 下","273KB");
picture2.insert("/Pic 下","1.23MB");
cartoon1.insert("/cartoon 下","80MB");
cartoon2.insert("/cartoon 下","81MB");
video.insert("/video 下","1.3GB");
}
}

运行结果


第十三章 代理模式

代理模式是常用的 对象结构型设计模式 之一,当无法直接访问某个对象时,可以通过一个代理对象来间接访问。

为了保证客户端使用的透明性,所访问的真实对象与代理对象需要实现相同的接口。

代理模式根据使用目的,又可以分为:保护代理、远程代理、虚拟代理、缓冲代理等。

一、代理模式概述

日常生活中,我们通过网站购买商品,商品对应的就是“真实主题角色”,而商品网站则是“代理主题角色”。

1.1 定义

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

代理模式通过引入一个新的代理对象,在客户端对象和目标对象之间起到中介的作用,去掉客户不能看到的内容和服务,或者增添客户需要的额外的新服务。

1.2 代理模式的结构

代理模式结构

代理模式的重点在于,1. 客户端针对 抽象角色 编程 2. 抽象对象实例化代理对象 3. 通过代理对象维护的真实角色对象,来调用真实业务,并通过代理对象实现的方法,来扩展真实业务员

1.3 代理模式中的几个角色

  1. Subject - 抽象主题角色
    1. 声明了真实主题和代理主题的共同接口
    2. 客户端通常只需要针对抽象主题角色进行编程
  2. Proxy - 代理主题角色
    1. 包含对真实主题的应用,可以在任何时候操作真实主题
    2. 代理主题角色提供与真实主题角色相同的接口,以便在任何时候替代真实主题
    3. 代理主题角色还能控制对真实主题的使用,负责在需要时创建、删除真实主题对象,对真实主题对象的使用加以约束
  3. RealSubject - 真实主题角色
    1. 定义了真实的业务操作
    2. 客户端可以通过代理角色,间接调用真实角色的操作

1.4 代理模式的核心 - 代理主题角色 Proxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Proxy implements Subject {
// 维护一个对真实主题对象的引用
private RealSubject subject = new RealSubject();

public void preRequest(){
// ...
}

/**
* 实现Subject声明的抽象方法,调用真实主题角色的操作,并进行扩展
*/
@Override
public void request(){
preRequest();
subject.request();
postRequest();
}

public void postRequest(){
// ...
}
}

二、几种常用的代理模式

  1. 远程代理 - Remote Proxy
    1. 为一个位于不同的地址空间的对象提供一个本地的代理对象
    2. 远程代理又称为 大使 - Ambassador
    3. 客户端无须关心实现具体业务的是谁,只需要按照服务接口定义的方式直接与本地主机中的代理对象交互即可
    4. DCOM、Web Service 都应用了远程代理模式
  2. 虚拟代理 - Virtual Proxy
    1. 如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建
      1. 通常可以结合多线程技术,一个线程用于显示代理对象,其他线程用于加载真实对象
  3. 保护代理 - Protect Proxy
    1. 控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限
  4. 缓冲代理 - Cache Proxy
    1. 为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果
  5. 智能引用代理 - Smart Reference Proxy
    1. 当一个对象被引用时,提供一些额外的操作,例如记录被调用的次数等

三、完整实现

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/**
* 代理模式
*/
// 数据库验证类 - 业务类
class AccessValidator {
public Boolean validate(String userId) {
System.out.println("在数据库中验证用户:" + userId + ",是否是合法用户?");
if (userId.contains("aa")) {
System.out.println("验证通过!");
return true;
} else {
System.out.println("验证失败!");
return false;
}
}
}

// 日志记录类 - 业务类
class Logger {
public void log(String userId) {
System.out.println("记录用户 " + userId + " 登录日志");
}
}

// 抽象查询类 - 抽象主题角色
interface Searcher {
String doSearch(String userId, String keyword);
}

// 具体查询类 - 真实主题角色
class RealSearcher implements Searcher {
@Override
public String doSearch(String userId, String keyword) {
System.out.println("用户 " + userId + " 使用关键词 " + keyword + " 进行查询");
return "查询到的具体内容";
}
}

// 代理查询类 - 代理主题角色,维持对业务类、真实主题角色对象的引用
class ProxySearcher implements Searcher {
// 维持一个对真实主题的引用
private RealSearcher searcher = new RealSearcher();

// 业务类
private AccessValidator validator = new AccessValidator();
private Logger logger = new Logger();

@Override
public String doSearch(String userId, String keyword) {
// 如果身份验证成功,则继续执行
if (this.validate(userId)) {
// 调用真实主题对象的查询方法
final String result = searcher.doSearch(userId, keyword);
// 记录查询日志
this.logger(userId);
// 返回查询结果
return result;
}
return null;
}

public boolean validate(String userId) {
return validator.validate(userId);
}

public void logger(String userId) {
logger.log(userId);
}
}

public class ProxyPattern {
public static void main(String[] args) {
final ProxySearcher searcher = new ProxySearcher();
final String search = searcher.doSearch("aa张三", "男士内衣");
System.out.println("查询结果:"+search);
}
}

执行结果如下:

执行结果

四、总结

优点

代理模式各类型的共同优点:

  1. 能够协调调用者和被调用者,一定程序上降低了系统耦合
  2. 客户端可以针对 抽象主题角色 进行编程,增加和更换代理类无须修改源代码,符合开闭原则,系统具有良好的灵活性和可扩展性

不同代理模式的特点:

  1. 远程代理,为两个不同地址空间对象的访问提供了一种实现机制,可以将一些消耗资源较多的对象和操作 移至性能更好的计算机上,提高系统的整体运行效率
  2. 虚拟代理通过一个消耗资源较少的对象来代表一个消耗资源较多的对象,可以在一定程度上节省系统的运行开销
  3. 缓冲代理,为某一个操作 的结果提供临时的缓存存储空间,以便在后续使用中能够共享这些结果,优化系统性能,缩短执行时间
  4. 保护代理,可以控制一个对象的访问权限,为不同用户提供不同级别的使用权限

缺点

  1. 由于增加了代理对象,可能会导致请求的处理速度变慢,如保护代理
  2. 实现代理模式需要额外的工作,实现过程可能变得十分复杂,如远程代理

适用场景

  1. 当客户端对象需要访问远程主机中的对象时,可以使用远程代理
  2. 当需要用一个消耗资源较少的对象来代表一个消耗资源较多的对象时,可以使用虚拟代理
    1. 从而降低系统开销、缩短运行时间
  3. 为某一个需要频繁访问的操作结果提供一个临时存储空间,以供多个客户端共享访问时,可以使用缓冲代理
  4. 需要控制对象的访问权限时,可以使用保护代理
  5. 需要为一个对象的引用提供一些额外操作时,可以使用智能引用代理

第十四章 责任链模式

一、设计一款请假程序

Sunny 软件公司的 OA 系统需要提供一个假条审批模块:

如果员工请假天数小于3天,主任可以审批;大于等于3天,小于10天,经理可以审批;大于等于10天,总经理审批;超过30天,拒绝。

1.1 初步设计

在没学责任链模式之前的设计可能如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ChainOfResponsibilityPattern {
public void handleRequset(Integer days) {
if(days < 3){
this.director(days);
}
else if(days < 10){
this.manager(days);
}
else if(days < 30){
this.gManager(days);
}
else {
throw new RuntimeException("error handler");
}
}

public void director(Integer days){
// 主任处理。。。
}

// .......
}

1.2 存在的问题

  1. ChainOfResponsibilityPattern 类十分的庞大,所有的审批方法都集中在一个类中,违反了“单一职责原则”,测试和维护难度大
  2. 如果需要增加一个新的审批登记,或者调整任意一级的审批权限,都需要修改源代码。违反了“开闭原则”
  3. 审批流程的设置缺乏灵活性,当设计完 主任》经理》总经理》异常 的流程后,再想修改,必须直接修改源代码,客户端无法定制流程

1.3 解决方案

使用责任链模式来重构。

二、责任链模式

2.1 定义

责任链模式 - Chain Of Responsibility Pattern:避免请求发送者和接收者耦合在一起,让多个对象都有可能接受请求,将这些请求连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。

责任链模式是一种对象行为型模式。

责任链模式

2.2 责任链模式中的几个角色

  1. Handler - 抽象处理者
    1. 定义了处理请求的抽象接口
    2. 定义一个抽象处理者类型的对象引用,作为对下家的引用
    3. 通过该引用,处理者可以形成一条链式调用
  2. ConcreteHandler - 具体处理者
    1. 具体处理者有两大作用
    2. 第一,处理请求,不同的处理者以不同形式实现抽象处理方法
    3. 第二,转发请求,如果请求不再当前处理者的权限内,可以将请求转发给下家

2.3 责任链模式的核心 - 抽象处理者类

1
2
3
4
5
6
7
8
9
abstract class Handler {
protected Handler successor;

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

public abstract void handleRequest(String request);
}

2.4 具体处理者

1
2
3
4
5
6
7
8
9
10
class ConcreteHandler extends Handler {
@Override
public void handleRequest(String request){
if(request满足条件){
// 处理请求
}else {
this.successor.handleRequest(request); // 转发请求
}
}
}

三、完整实现

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
* 职责链模式
* Sunny 软件公司的 OA 系统需要提供一个假条审批模块:
* 如果员工请假天数小于3天,主任可以审批
* 大于等于3天,小于10天,经理可以审批
* 大于等于10天,总经理审批;超过30天,拒绝
*/
// 抽象处理者 - 经理、主任等具体处理者的抽象父类
abstract class Handler {
protected Handler successor;

// 指向下一个具体实现类的引用,通过该引用,处理者可以连成一条线
public void setSuccessor(Handler successor) {
this.successor = successor;
}

// 抽象处理方法,交给具体实现类实现
protected abstract void handleRequest(Integer days);
}

// 主任 - 具体处理者
class DirectorHandler extends Handler {

// 如果在处理范围内,则进行处理,否则继续向下转发
@Override
protected void handleRequest(Integer days) {
if (days < 3) {
System.out.println("角色[主任],处理了员工请假请求,请假天数为[" + days + "]天");
} else {
super.successor.handleRequest(days);
}
}
}

// 经理 - 具体处理者
class ManagerHandler extends Handler {
@Override
protected void handleRequest(Integer days) {
if (days < 10)
System.out.println("角色[经理],处理了员工请假请求,请假天数为[" + days + "]天");
else
super.successor.handleRequest(days);
}
}

// 总经理 - 具体处理者
class GManagerHandler extends Handler {
@Override
protected void handleRequest(Integer days) {
if (days < 30)
System.out.println("角色[总经理],处理了员工请假请求,请假天数为[" + days + "]天");
else
super.successor.handleRequest(days);
}
}

// 错误 - 具体处理者
class ErrorHandler extends Handler {
@Override
protected void handleRequest(Integer days) {
System.out.println("请假失败,当前请假天数[" + days + "]超过最大限制!");
}
}

public class ChainOfResponsibilityPattern {

public static void main(String[] args) {
Handler director = new DirectorHandler();
Handler manager = new ManagerHandler();
Handler gManager = new GManagerHandler();
Handler error = new ErrorHandler();

director.setSuccessor(manager);
manager.setSuccessor(gManager);
gManager.setSuccessor(error);

director.handleRequest(3);
director.handleRequest(11);
director.handleRequest(44);
}
}

四、纯与不纯的责任链模式

4.1 纯的责任链模式

纯责任链模式,要求一个具体处理者对象只能在两个行为中选择一个,要么承担全部责任,要么将责任推给下家。

4.2 不纯的责任链模式

不纯的责任链模式,允许某个请求被一个具体处理者部分处理后向下传递,或者该请求能够被多个处理器处理。

五、总结

优点

  1. 责任链模式使一个对象无须知道是哪个对象处理其请求,只需要知道该请求会被处理即可。接收者与发送者互不相知,且链中对象不需要知道链的结构,由客户端负责链的创建,降低了系统的耦合度
  2. 请求处理对象仅需维持一个向后继者的引用,不需要维持所有后继者的引用,简化对象的相互连接
  3. 在给对象分派职责时,责任链可以给我们更多的灵活性,可通过在运行时对该链动态增加或修改一个请求的职责
  4. 新增请求无需修改原有代码,只需重新建链即可,符合“开闭原则”

缺点

  1. 不能保证请求必定会被执行,该请求可能到链的末尾也没有匹配到合适的处理者;或者责任链的配置有误,也可能导致请求未被处理
  2. 如果责任链较长,请求的处理会涉及多个对象,系统性能会受到一定影响,且在调试时不太方便
  3. 如果链表建立有误,可能会陷入循环调用

适用场景

  1. 有多个对象可以处理同一个请求,具体哪个对象处理该请求待运行时刻再确定,客户端只需将请求提交到连上,无须关心请求的处理对象是谁,以及如何处理的
  2. 在不明确指定接收者的情况下,向多个对象中的一个,提交一个请求
  3. 动态指定一组对象处理请求,可以动态创建责任链,动态改变链中处理者的先后次序等

第十五章 命令模式 - 请求发送者和接收者解耦

一、命令模式

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

命令模式是一种对象行为型模式,又称为动作模式或事务模式。

命令模式中包含的几个角色:

  1. Command - 抽象命令类
    1. 声明了用于执行请求的 execute() 等方法,通过这些方法可以调用请求接收者的相关操作
  2. ConcreteCommand - 具体命令类
    1. 实现抽象命令类中声明的方法
    2. 对应具体的接收者对象,将接收者对象的动作绑定其中
    3. 在实现execute() 方法时,调用接收者对象的相关操作
  3. Invoker - 调用者
    1. 请求的发送者,通过命令对象来执行请求
    2. 调用者并不需要在设计时确定其接收者,因此它只与抽象命令类保持关联
    3. 在程序运行时,注入一个具体命令对象,再调用具体命令对象的execute() 方法
    4. 从而间接调用请求接收者的相关操作
  4. Receiver - 接收者
    1. 接收者执行与请求相关的操作,它具体实现对请求的业务处理

命令模式的本质是对请求进行封装,一个请求对应一个命令,将发出命令的责任和执行命令的责任分隔开。

二、命令模式的关键 - 抽象命令类

请求发送者只需要针对抽象命令类编程即可。

1
2
3
4
5
// 抽象命令类
abstract class Command {
// 声明公共的执行方法
public abstract void execute();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 请求发送者 - 调用者
class Invoker {
// 请求发送者只需要针对 Command 编程即可,具体命令类在运行时指定
private Command command;

// 通过构造函数或者setter方法注入Command
public Invoker(Command command){
this.command = command;
}

// 用于调用命令类的 execute 方法
public void call(){
command.execute();
}
}
1
2
3
4
5
6
7
8
9
10
// 具体命令类
class ConcreteCommand extends Command {
// 维持一个对请求接收者对象的引用
private Receiver receiver; // 如果需要多个接收者,可以将该对象改为List等集合对象

public void execute(){
// 调用请求接收者的业务处理方法
receiver.action();
}
}
1
2
3
4
5
class Receiver {
public void action(){
// 具体操作
}
}

三、完整实现

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// 功能键设计窗口类
class FBSettingWindow{
// 窗口标题
private String title;
// List集合,存储所有的功能按键
private List<FunctionButton> functionButtons = new ArrayList<>();

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public void addFunctionButton(FunctionButton fb){
functionButtons.add(fb);
}

public void removeFunctionButton(FunctionButton fb){
functionButtons.remove(fb);
}

// 显示窗口及功能
public void display(){
System.out.println("显示窗口:"+this.title);
System.out.println("显示功能键");
for (FunctionButton functionButton : functionButtons) {
System.out.println(functionButton.getName());
}
}
}

// 功能按键 - 请求发送者
class FunctionButton {
// 按键名称
private String name;
// 维持一个抽象命令的引用
private Command command;

public String getName() {
return name;
}

public Command getCommand() {
return command;
}

public void setName(String name) {
this.name = name;
}

public void setCommand(Command command) {
this.command = command;
}

public void onClick(){
System.out.println("点击按键:");
command.execute();
}
}

// 抽象命令类
abstract class Command {
public abstract void execute();
}

// 帮助命令类 - 具体命令类
class HelpCommand extends Command {
private HelpHandler helpHandler;

public HelpCommand() {
helpHandler = new HelpHandler();
}

@Override
public void execute() {
helpHandler.action();
}
}

// 最小化命令类 - 具体命令类
class MinimizeCommand extends Command {
private MinimizeHandler handler;

public MinimizeCommand() {
handler = new MinimizeHandler();
}

@Override
public void execute() {
handler.action();
}
}

// 帮助处理类 - 请求接收者
class HelpHandler {
public void action(){
System.out.println("显示文档");
}
}

// 最小化处理类 - 请求接收者
class MinimizeHandler {
public void action (){
System.out.println("最小化!");
}
}

public class CommandPattern {
public static void main(String[] args) {
final FBSettingWindow window = new FBSettingWindow();
window.setTitle("功能键设置");

final FunctionButton button1 = new FunctionButton();
button1.setName("按钮1");
final FunctionButton button2 = new FunctionButton();
button2.setName("按钮2");

final HelpCommand helpCommand = new HelpCommand();
final MinimizeCommand minimizeCommand = new MinimizeCommand();

button1.setCommand(helpCommand);
button2.setCommand(minimizeCommand);

window.addFunctionButton(button1);
window.addFunctionButton(button2);
window.display();

button1.onClick();
button2.onClick();
}
}

如果需要增加一个新的命令,只需要增加一个具体命令类,将命令类与具体的处理类进行关联,并注入到某个功能键即可。原有代码无需修改,符合“开闭原则”。

四、命令队列

当我们点击一个按钮后,需要执行多次命令操作,且命令是可重用的。此时我们可以使用“命令队列”的方式来设计,此时,请求发送者不再维护单独的一个Command,而是一个CommandQueue,存储多个命令对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class FunctionButton {
private String name;
private CommandQueue commandQueue;
}
class CommandQueue {
private List<Command> commands = new ArrayList<>();

public void addCommand(Command command){
commands.add(command);
}

public void removeCommand(Command command){
commands.remove(command);
}

public void display(){
for(Command cd:commands){
cd.execute();
}
}
}

命令队列类似于“批处理”的概念,可以对一组对象(命令)进行批量操作。如果请求接收者没有严格的先后次序,还可以通过多线程技术来并发调用对象的execute方法,从而提高程序执行效率。

五、撤销操作的实现

在命令模式中,可以通过调用命令对象的 execute() 方法来实现对请求的处理,如果需要撤销操作,可以在命令类中增加一个逆向操作来实现。

也可以通过保存对象的历史状态来实现撤销,如“备忘录模式”。

我们可以在具体的Handler 中,保存上次执行时的状态,并提供一个方法以便恢复至保存的状态。

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
class AdderHandler{
private int number;

public int add(int i){
number += i;
return number;
}
}

class AdderCommand extends Command {
private AdderHandler handler;
private int history;

public AdderCommand(AdderHandler handler){
this.handler = handler;
}

@Override
public void add(int value){
int result = handler.add(value);
history = value;
System.out.print("加法结果为:"+result);
}

@Override
public void undo(){
// 使用保存的状态实现撤销操作
int result = handler.add(-history);
System.out.print("撤销后的结果为:"+result);
}
}

注,该撤销操作只能执行一次,如果需要执行多次,可以使用集合来保存 histroy

六、宏命令

宏命令(Macro Command)又称为组合命令,是组合模式和命令联合的产物。

宏命令是一个具体的命令类,拥有一个集合属性,在该集合中包含了对其他命令对象的引用。通常宏命令不直接与请求接收者交互,而是通过它的成员来调用接收者的方法。

当调用宏命令的 execute 方法时,会递归调用它的每个成员命令的 execute 方法,成员既可以是一个简单命令,也可以是宏命令。

七、总结

命令模式是一种 使用频率非常高 的设计模式,可以将请求发送者和接收者解耦。发送者通过命令对象间接引用请求接收者,使得系统具有更好的灵活性和可扩展性。

优点

  1. 降低系统耦合,请求发送者与接收者不直接引用,相互独立
  2. 方便扩展,增加新的命令不会影响其他类,无需修改系统代码,符合“开闭原则”
  3. 可以比较简单的设计一个命令队列或组合命令(宏命令)
  4. 为请求的撤销、恢复操作提供了一种设计和实现方案

缺点

  1. 可能会导致系统中存在过多的命令类,因为每个接收者都需要设计一个具体命令类来调用

适用场景

  1. 系统需要请求发送者与接收者解耦,彼此互不影响,互不相知
  2. 系统需要使用一组命令或者宏命令
  3. 系统需要支持撤销、恢复操作
  4. 系统需要在不同的时间指定请求、将请求排队和执行请求

八、练习

1
2
3
4
5
6
7
8
9
10
11
12
命令模式
Sunny软件公司欲开发一个基于Windows平台的公告板系统。
该系统提供了一个主菜单(Menu),
在主菜单中包含了一些菜单项(MenuItem),
可以通过Menu类的addMenuItem()方法增加菜单项。
菜单项的主要方法是click(),
每一个菜单项包含一个抽象命令类,
具体命令类包括OpenCommand(打开命令),CreateCommand(新建命令),EditCommand(编辑命令)等,
命令类具有一个execute()方法,
用于调用公告板系统界面类(BoardScreen)的open()、create()、edit()等方法。
试使用命令模式设计该系统,
以便降低MenuItem类与BoardScreen类之间的耦合度
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
/**
* 命令模式
* Sunny软件公司欲开发一个基于Windows平台的公告板系统。
* 该系统提供了一个主菜单(Menu),
* 在主菜单中包含了一些菜单项(MenuItem),
* 可以通过Menu类的addMenuItem()方法增加菜单项。
* 菜单项的主要方法是click(),
* 每一个菜单项包含一个抽象命令类,
* 具体命令类包括OpenCommand(打开命令),CreateCommand(新建命令),EditCommand(编辑命令)等,
* 命令类具有一个execute()方法,
* 用于调用公告板系统界面类(BoardScreen)的open()、create()、edit()等方法。
* 试使用命令模式设计该系统,
* 以便降低MenuItem类与BoardScreen类之间的耦合度
*/
// 主菜单 - 类似于之前的窗口界面,包含多个按钮(菜单项)
class Menu{
private List<MenuItem> menuItems = new ArrayList<>();

public void addItem(MenuItem item){
menuItems.add(item);
}

public void display(){
System.out.println("------显示菜单:-----");
for (MenuItem menuItem : menuItems) {
System.out.println(menuItem.getName());
}
}
}

// 菜单项 - 请求发送者
class MenuItem {
private String name;
private Command command;

public MenuItem(String name){
this.name = name;
}

public String getName() {
return name;
}

public void setCommand(Command command){
this.command = command;
}

public void create(){
command.create();
}

public void edit(){
command.edit();
}

public void open(){
command.open();
}
}

// 抽象命令类
abstract class Command {
abstract void create();
abstract void edit();
abstract void open();
}

// 宏命令 - 一组命令的集合,宏命令一般不直接操作接收者,而是通过集合中的对象属性来操作
class MacroCommand extends Command {
private List<Command> commands = new ArrayList<>();

public void addCommand(Command command){
this.commands.add(command);
}

@Override
void create() {
for (Command command : commands) {
command.create();
}
}

@Override
void edit() {
for (Command command : commands) {
command.edit();
}
}

@Override
void open() {
for (Command command : commands) {
command.open();
}
}
}

// 具体命令类
class BoardScreenCommand extends Command{
private BoardScreenHandler handler;

public BoardScreenCommand(){
handler = new BoardScreenHandler();
}

@Override
void create() {
handler.create();
}

@Override
void edit() {
handler.edit();
}

@Override
void open() {
handler.open();
}
}

// 请求接收者,也就是具体的处理类
class BoardScreenHandler {
public void open(){
System.out.println("打开公告板");
}
public void create(){
System.out.println("创建公告板");
}
public void edit(){
System.out.println("修改公告板");
}
}

public class CommandPattern {
public static void main(String[] args) {
final Menu menu = new Menu();

final MenuItem menuItem = new MenuItem("公告板管理");
final MenuItem menuItem1 = new MenuItem("高噶");

final Command command = new BoardScreenCommand();

final MacroCommand command1 = new MacroCommand();
command1.addCommand(command);
command1.addCommand(command);

menuItem.setCommand(command);
menuItem1.setCommand(command1);

menu.addItem(menuItem);
menu.addItem(menuItem1);

menu.display();
menuItem.create();
menuItem.open();
System.out.println("----宏命令执行------");
menuItem1.create();
menuItem1.edit();
menuItem1.open();
}
}

第十六章 解释器模式 - 自定义语言的实现

一、什么是解释器模式

解释器模式(Interpreter Pattern):定义一个语言的文法,并且建立一个解释器来解释该语言中的句子,这里的“语言”是指使用规定格式和语法的代码。解释器模式是一种类行为型模式。

在某些情况下,为了更好地描述某一些特定类型的问题,我们可以创建一种新的语言,这种语言拥有自己的表达式和结构,即文法规则,这些问题的实例将对应为该语言中的句子。

二、文法规则和抽象语法树

解释器模式描述了如何为简单的语言定义一个文法,如何在改语言中表示一个句子,以及如何解释这些句子。

在解释器模式中,我们可以使用一些符号来表示不同的含义,如“|”表示或,“{”与“}”表示组合等等。这些符号即代表着部分的文法规则

除了依靠文法规则来定义一个语言之外,还可以通过抽象语法树的图形方式来直观的表示语言的构成。

抽象语法树示意图

例如一个表达式语句 “1+2+3-4+1”,

2. 解释器模式中包含的几个角色:

  1. AbstractExpression - 抽象表达式
    1. 在抽象表达式中声明了抽象的解释操作,他是所有终结符表达式和非终结符表达式的公共父类
  2. TerminalExpression - 终结符表达式
    1. 是抽象表达式的子类,实现了与文法中的终结符相关联的解释操作
    2. 在句子中的每一个终结符都是该类的一个实例
    3. 通常在一个解释器模式中只有少数几个终结符表达式,他们的实例可以通过非终结符表达式组成较为复杂的句子
  3. NonterminalExpression - 非终结符表达式
    1. 抽象表达式的子类,实现了文法中非终结符的解释操作
    2. 由于在非终结符表达式中可以包含终结符表达式,也可以包含非终结符表达式
    3. 因此其解释操作通常是递归的方式
  4. Context - 环境类
    1. 环境类又称为上下文类,用于存储解释器之外的一些全局信息
    2. 通常临时存储了需要解释的语句

3. 核心类的设计

抽象表达式类

在解释器模式中,每一种终结符和非终结符都有一个具体类与之对应,正因为使用类来表示每一条文法规则,所以系统将具有较好的灵活性和可扩展性。

1
2
3
abstract class AbstractExpression {
public abstract void interpret(Context ctx);
}

终结符表达式

终结符表达式主要是对终结符元素的处理。

1
2
3
4
5
class TerminalExpression extends AbstractExpression {
public void interpre(Context ctx) {
// 终结符表达式的解释操作
}
}

非终结符表达式

非终结符表达式将表达式组合成更加复杂的结构,对于包含两个操作元素的非终结符表达式类,其典型代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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() 方法
// 在递归调用时指定组成部分的连接方式,即非终结符的功能
}
}

上下文类

环境类Context, 用于存储一些全局信息,通常在Context中包含了一个 HashMap 或者 ArrayList 等类型的集合对象。

1
2
3
4
5
6
7
8
9
10
class Context {
private HashMap map = new HashMap();
public void assign(String key, String value){
// 往环境中设值
}

public String lookup(String key) {
// 获取存储在环境中的值
}
}

系统可以根据西药来决定是否需要环境类

三、完整实现

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
// 抽象表达式
abstract class AbstractNode {
public abstract String interpret();
}

// And解释 - 非终结符表达式
class AndNode extends AbstractNode {
private AbstractNode left;
private AbstractNode right;

public AndNode(AbstractNode left, AbstractNode right){
this.left = left;
this.right = right;
}

// And 表达式的解释操作
public String interpret(){
return left.interpret() + "再" + right.interpret();
}
}

// 简单句子解释 - 非终结符表达式
class SentenceNode extends AbstractNode {
private AbstractNode direction;
private AbstractNode action;
private AbstractNode distance;

public SentenceNode(AbstractNode direction, AbstractNode action, AbstractNode distance) {
this.direction = direction;
this.action = action;
this.distance = distance;
}

public String interpret(){
return direction.interpret() + action.interpret() + distance.interpret();
}
}

// 方向解释 - 终结符表达式
class DirectionNode extends AbstractNode {
private String direction;

public Direction(String direction) {
this.direction = direction;
}

public String interpret() {
switch(direction){
case "up":
return "向上";
case "down":
return "向下";
case "left":
return "向左";
case "right":
return "向右";
default:
return "无效指令"'
}
}
}

// 动作解释 - 终结符表达式
class ActionNode extends AbstractNode {
private String action;

public ActionNode(String action) {
this.action = action;
}

public String interpret() {
switch(action) {
case "move":
return "移动";
case "run":
return "快速移动";
default:
return "无效指令";
}
}
}

// 距离解释 - 终结符表达式
class DistanceNode extends AbstractNode {
private String distance;

public DistanceNode(String distance) {
this.distance = distance;
}

public String interpret() {
return distance;
}
}

// 指令处理类 - 工具类
/**
* 工具类用于对输入指令进行处理,将指令分隔为字符串数组
* 将第一二三个单词组成一个句子,并存入栈中
* 如果发现单词 “and”,则将 “and” 后的第一二三个单词组成一个新的句子作为右表达式,并从栈中取出原先的句子作为左表达式,然后组合成一个And节点存入栈中
* 依次类推,直到整个指令解析结束
*/
class InstructionHandler {
private String instruction;
private AbstractNode node;

public void handle(String instruction) {
AbstractNode left = null;
AbstractNode right = null;

AbstractNode direction = null;
AbstractNode action = null;
AbstractNode distance = null;

// 声明一个栈对象用于存储抽象语法树
Stack stack = new Stack();
// 以空格分隔指令字符串
String[] words = instruction.split(" ");

for(int i = 0;i < words.length; i++){
// 如果遇到 and,则将后三个单词作为三个终结符表达式连成一个简单句子,并作为右表达式
// 从栈顶弹出之前的表达式作为 and 的左表达式,最后将and压入栈中
if(words[i].equalsIgnoreCase("and")){
// 弹出栈顶表达式作为左表达式
left = (AbstractNode) stack.pop();
direction = new DirectionNode(words[++i]);
action = new ActionNode(words[++i]);
distance = new DistanceNode(words[++i]);
// 右表达式
right = new SentenceNode(direction, action, distance);
// 将新表达式压入栈中
stack.push(new AndNode(left, right));
}
// 如果是从头开始解释,则将前三个单词组成一个简单句子并压入栈中
else {
direction = new DirectionNode(words[i]);
action = new ActionNode(words[++i]);
distance = new DistanceNode(words[++i]);
left = new SentenceNode(direction, action, distance);
stack.push(left);
}
}

this.node = (AbstractNode)stack.pop();
}

public String output(){
// 解释表达式
String result = node.interpret();
return result;
}
}

// 客户端 - 测试类
class Client {
public static void main(String[] args) {
String instruction = "up move 5 and down run 10 and left move 5";
InstructionHandler handler = new InstructionHandler();
handler.handle(instruction);
String outString;
outString = handler.output();
System.out.println(outString);
}
}

四、Context的作用

在解释器模式中,环境类Context用于存储解释器之外的一些全局信息,它通常作为参数被传递到所有表达式的解释方法 interpret() 中,可以在 Context 对象中存储访问表达式解释器的状态,向表达式解释器提供一些全局的、公共的数据,此外还可以在 Context 中增加一些所有表达式解释器都共有的功能,减轻解释器的职责。

五、总结

优点

  1. 易于改变和扩展文法。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法
  2. 每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言
  3. 实现文法较为容易,在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂,还可以通过一些工具自动生成节点类代码
  4. 增加新的解释表达式较为方便。如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无需修改,符合“开闭原则”

缺点

  1. 对于复杂文法难以维护,每一条规则至少对应一个类
  2. 执行效率较低,模式中使用了大量的循环和递归调用,因此在解释时速度很慢

适用场景

  1. 讲一个需要解释执行的语言中的句子表示为一个丑行语法树
  2. 一些重复出现的问题可以用一种简单的语言来进行表达
  3. 一个语言的文法较为简单
  4. 执行效率不是关键问题

六、练习

Sunny软件公司欲为数据库备份和同步开发一套简单的数据库同步指令,通过指令可以对数据库中的数据和结构进行备份,例如,输入指令“COPY VIEW FROM srcDB TO desDB”表示将数据库srcDB中的所有视图(View)对象都拷贝至数据库desDB;输入指令“MOVE TABLE Student FROM srcDB TO esDB”表示将数据库srcDB中的Student表移动至数据库desDB。试使用解释器模式来设计并实现该数据库同步指令。

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
import java.util.Stack;

/**
* 解释器模式
* Sunny软件公司欲为数据库备份和同步开发一套简单的数据库同步指令,
* 通过指令可以对数据库中的数据和结构进行备份,
* 例如,
* 输入指令“COPY VIEW FROM srcDB TO desDB”
* 表示将数据库srcDB中的所有视图(View)对象都拷贝至数据库desDB;
* 输入指令“MOVE TABLE Student FROM srcDB TO esDB”
* 表示将数据库srcDB中的Student表移动至数据库desDB。
* 试使用解释器模式来设计并实现该数据库同步指令。
*/
// 抽象指令
abstract class AbstractOrder {
// 抽象公共方法,用于解释具体的指令
abstract String interpret();
}

// 从。。到。。 指令 - 非终结符表达式
class FromOrder extends AbstractOrder {
private AbstractOrder first;
private AbstractOrder second;

public FromOrder(AbstractOrder first, AbstractOrder second) {
this.first = first;
this.second = second;
}

@Override
String interpret() {
return "从" + first.interpret() + "到" + second.interpret();
}
}

// 视图指令 - 终结符表达式
class ViewOrder extends AbstractOrder {
private String view;

public ViewOrder() {
}

public ViewOrder(String view) {
this.view = view;
}

@Override
String interpret() {
if (view == null) {
return "[所有视图]";
}
return "[视图:" + view + "]";
}
}

// 表格指令 - 终结符表达式
class TableOrder extends AbstractOrder {
private String table;

public TableOrder() {
}

public TableOrder(String table) {
this.table = table;
}

@Override
String interpret() {
if (table == null) {
return "[所有表格]";
}
return "[" + table + "表]";
}
}

// 数据库名称指令 - 终结符表达式
class DBOrder extends AbstractOrder {
private String name;

public DBOrder(String name) {
this.name = name;
}

@Override
String interpret() {
return "[数据库:" + name + "]";
}
}

// 备份指令 - 终结符表达式
class CopyOrder extends AbstractOrder {
@Override
String interpret() {
return "拷贝";
}
}

// 移动指令 - 终结符表达式
class MoveOrder extends AbstractOrder {
@Override
String interpret() {
return "移动";
}
}

// 句子解释(Copy/Move FromOder) - 非终结符表达式
class SentenceOrder extends AbstractOrder {
private AbstractOrder direction;
private AbstractOrder action;
private AbstractOrder distance;

public SentenceOrder(AbstractOrder direction, AbstractOrder action, AbstractOrder distance) {
this.direction = direction;
this.action = action;
this.distance = distance;
}

@Override
String interpret() {
return direction.interpret() + action.interpret() + distance.interpret();
}
}

// 指令工具类
class InstructionHandler {
private String instruction; // 输入的指令字符串
private AbstractOrder order; // 字符串转换后的指令

public void handle(String instruction) {
AbstractOrder direction; // sentence 指令的 direction
AbstractOrder action; // sentence 指令的 action
AbstractOrder distance; // sentence 指令的 distance

// 使用栈来保存表达式
Stack<AbstractOrder> stack = new Stack<>();

// 将输入的指令字符串按照 空格 拆分
final String[] words = instruction.split(" ");

for (int i = 0; i < words.length; i++) {
final String word = words[i];

// 如果为 from 指令,则解析 From [DB] TO [DB]
switch (word.toLowerCase()) {
case "from":
String f = words[++i];
++i;
String s = words[++i];
stack.push(new FromOrder(new DBOrder(f), new DBOrder(s)));
break;
case "copy":
stack.push(new CopyOrder());
break;
case "move":
stack.push(new MoveOrder());
break;
case "view":
if (!words[i + 1].equalsIgnoreCase("from"))
stack.push(new ViewOrder(words[++i]));
else
stack.push(new ViewOrder());
break;
case "table":
if (!words[i + 1].equalsIgnoreCase("from"))
stack.push(new TableOrder(words[++i]));
else
stack.push(new TableOrder());
break;
default:
throw new RuntimeException("无效指令");
}
}
distance = stack.pop();
action = stack.pop();
direction = stack.pop();
// order 最终是一个解释类表达式
order = new SentenceOrder(direction, action, distance);
}

public String output() {
return order.interpret();
}
}

public class InterpretPattern {
public static void main(String[] args) {
String order = "MOVE TABLE Student FROM srcDB TO esDB";
final InstructionHandler handler = new InstructionHandler();
handler.handle(order);
final String output = handler.output();
System.out.println(output);
}
}


第十七章 迭代器模式 - 遍历聚合对象中的元素