1. 行为型模式

行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。

行为型模式分为类行为模式对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。

行为型模式分为:

  • 模板方法模式 (TemplateMethod)
  • 策略模式 (Strategy)
  • 命令模式 (Command)
  • 职责链模式 (ChainOfResponsibility)
  • 状态模式 (State)
  • 观察者模式 (Observer)
  • 中介者模式 (Mediator)
  • 迭代器模式 (Iterator)
  • 访问者模式 (Visitor)
  • 备忘录模式 (Memento)
  • 解释器模式 (Interpreter)
    %%
    2129-🏡⭐️◼️行为型设计模式有哪些◼️⭐️-point-202301222129%%

    以上 11 种行为型模式,除了模板方法模式和解释器模式是类行为型模式,其他的全部属于对象行为型模式。
    %%
    2130-🏡⭐️◼️23 种设计模式都有什么?1. 创建者模式:单例模式、工厂模式、抽象工厂模式、原型模式、建造者模式;2. 结构型模式:代理模式、适配器模式、装饰者模式、桥接模式、外观模式、组合模式、享元模式;3. 行为型模式:模板方法模式、策略模式、命令模式、责任链模式、状态模式、观察者模式、中介模式、迭代器模式、访问者模式、备忘录模式、解释器模式◼️⭐️-point-202301222130%%

2. 模板模式概述

在面向对象程序设计过程中,程序员常常会遇到这种情况:==设计一个系统时知道了算法所需的关键步骤,而且确定了这些步骤的执行顺序==,但某些步骤的具体实现还未知,或者说某些步骤的实现与具体的环境相关。

例如,去银行办理业务一般要经过以下 4 个流程:取号、排队、办理具体业务、对银行工作人员进行评分等,==其中取号、排队和对银行工作人员进行评分的业务对每个客户是一样的==,可以在父类中实现,但是办理具体业务却因人而异,它可能是存款、取款或者转账等,可以延迟到子类中实现。

2.1. 定义

定义一个操作中的算法骨架 (相同的步骤及步骤顺序都是一样的),而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。

3. 模式结构⭐️🔴

模板方法(Template Method)模式包含以下主要角色:

3.1. 抽象类(Abstract Class)

负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成
1. 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法相同的地方
2. 基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:
- 抽象方法 (Abstract Method)变化的地方 一个抽象方法由抽象类声明、由其具体子类实现。%%
2205-🏡⭐️◼️模板方法中不变与变化的地方分别怎么体现和处理?不变的地方,放到抽象类的模板方法中。变化的地方,放到抽象类的抽象方法中,让子类去实现◼️⭐️-point-202301222205%%

- 具体方法 (Concrete Method) :一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。
- 钩子方法 (Hook Method) :在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。
一般钩子方法是用于判断的逻辑方法,这类方法名一般为 isXxx,返回值类型为 boolean 类型。

3.2. 具体子类(Concrete Class)

实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。

4. UML 图示⭐️🔴

image.png

对原理类图的说明——即模板方法模式的角色和职责

  • AbstractClass 抽象类中实现了模板方法,定义了算法的骨架,具体子类需要去实现其抽象方法或重写其中方法
  • ConcreteClass 实现了抽象方法,已完成算法中特定子类的步骤

5. 实现逻辑⭐️🔴

在抽象类中
1. 定义 final 类型的模板方法,将不变的内容封装起来
2. 定义普通方法,是模板方法中封装的不变的部分
3. 定义抽象方法,是变化的部分,让子类实现
4. 定义钩子函数,也是变化的部分,让子类根据情况实现
在实现类中,实现需要实现的方法

继承抽象类 + 封装不变逻辑 + 下发变化逻辑
%%
2206-🏡⭐️◼️模板方法的核心实现逻辑是什么?🔜📝 继承抽象类,封装不变逻辑,下发变化逻辑,还可以定义钩子函数,让子类根据情况实现◼️⭐️-point-202301222206%%

6. 案例分析⭐️🔴

6.1. 炒菜

炒菜的步骤是固定的,分为倒油、热油、倒蔬菜、倒调料品、翻炒等步骤。现通过模板方法模式来用代码模拟。类图如下:

%%
2206-🏡⭐️◼️模板方法的例子是什么?炒菜、做豆浆◼️⭐️-point-202301222206%%

6.2. 作豆浆

编写制作豆浆的程序,说明如下:

  • 1)制作豆浆的流程选材 —-> 添加配料 —-> 浸泡 —-> 放到豆浆机打碎
  • 2)通过添加不同的配料,可以制作出不同口味的豆浆
  • 3)选材、浸泡和放到豆浆机打碎这几个步骤对于制作每种口味的豆浆都是一样的
  • 4)请使用模板方法模式完成

说明:因为模板方法模式比较简单,很容易就想到这个方案,因此就直接使用,不再使用传统的方案来引出模板方法模式

image.png

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 SoyaMilk {  

//模板方法, make , 模板方法可以做成final , 不让子类去覆盖.
final void make() {
select();
addCondiments();
soak();
beat();
}

//选材料
void select() {
System.out.println("第一步:选择好的新鲜黄豆 ");
}

//添加不同的配料, 抽象方法, 子类具体实现
abstract void addCondiments();

//浸泡
void soak() {
System.out.println("第三步, 黄豆和配料开始浸泡, 需要3小时 ");
}

void beat() {
System.out.println("第四步:黄豆和配料放到豆浆机去打碎 ");
}
}

6.2.1. 钩子方法

  • 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
public abstract class SoyaMilk {
public final void make() {
// ...
if (customAddIngredients()) {
addIngredients();
}
// ...
}
// ...
}

/**
* 纯豆浆
*/
public class PureSoyaMilk extends SoyaMilk {
public PureSoyaMilk() {
System.out.println("============纯豆浆============");
}

@Override
protected void addIngredients() {
// 空实现即可
}

@Override
protected Boolean customAddIngredients() {
return false;
}
}

7. ⭐️🔴优缺点

7.1. 优点

  • 提高代码复用性
    将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中。
  • 实现了反向控制
    通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制 ,并符合“开闭原则”。例如 InputStream 中的 int c = read() 调用的就是子类实现的方法

7.2. 缺点

  • 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
  • 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
    %%
    2210-🏡⭐️◼️模板方法存在什么问题?🔜📝 每一种不同的算法都需要一个子类去实现,容易使系统变得臃肿。钩子函数使得子类的实现控制父类结果,这种反向控制让代码可读性变差◼️⭐️-point-202301222210%%

8. ⭐️🔴适用场景

  • 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
  • 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。使用钩子函数

9. ⭐️🔴JDK 源码分析

%%
2211-🏡⭐️◼️JDK 源码中哪里用到了模板方法模式?🔜📝 InputStream 中的 read 方法有 3 个参数,最终调用抽象方法 read,子类实现不同逻辑不同,这里用到的就是模板方法模式。Spring 的 refresh 方法中,用了好多抽象方法让子类实现,还有钩子函数等,是一个典型的模板模式的应用◼️⭐️-point-202301222211%%

9.1. InputStream

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 InputStream implements Closeable {
//抽象方法,要求子类必须重写
public abstract int read() throws IOException;

public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}

public int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}

int c = read(); //调用了无参的read方法,该方法是每次读取一个字节数据
if (c == -1) {
return -1;
}
b[off] = (byte)c;

int i = 1;
try {
for (; i < len ; i++) {
c = read();
if (c == -1) {
break;
}
b[off + i] = (byte)c;
}
} catch (IOException ee) {
}
return i;
}
}

从上面代码可以看到,无参的 read() 方法是抽象方法,要求子类必须实现。而 read(byte b[]) 方法调用了 read(byte b[], int off, int len) 方法,所以在此处重点看的方法是带三个参数的方法。在该方法中第 18 行、27 行,可以看到调用了无参的抽象的 read() 方法。
总结如下: 在 InputStream 父类中已经定义好了读取一个字节数组数据的方法是每次读取一个字节,并将其存储到数组的第一个索引位置,读取 len 个字节数据。具体如何读取一个字节数据呢?由子类实现。

9.2. AbstractApplicationContext

AbstractApplicationContext.java 中有一个 refresh() 方法就是模板方法,其中定义了抽象方法和钩子方法

image.png

10. 实战经验

  • 1)基本思想算法只存在于一个地方,也就是在父类中,容易修改。需要修改算法时,只要修改父类的模板方法或者已经实现的某些步骤,子类就会继承这些修改
  • 2)实现了最大化代码复用。父类的模板方法和已实现的某些步骤会被子类继承而直接使用
  • 3)既统一了算法,也提供了很大的灵活性。父类的模板方法确保了算法的结构保持不变,同时由子类提供部分步骤的实现
  • 4)一般模板方法都加上 final 关键字,防止子类重写模板方法

11. 参考与感谢

设计模式-2、设计模式及设计原则