封面来源:模板方法设计模式 (refactoringguru.cn),如有侵权,请联系删除。

Java 设计模式学习网站:Java设计模式:23种设计模式全面解析(超级详细)

菜鸟教程:模板模式

0. 前言

根据 GOF 所编写的《设计模式——可复用面向对象软件的基础》一书,设计模式共有 23 种,但我们在实际开发中常用的就那么几种,本文将介绍【模板方法模式】,也是我将介绍的最后一种常用设计模式。

1. 模式的定义与特点

那么什么是【模板方法模式】?

在大学毕业之际,大多数学校的大多数专业都会有毕业答辩这样一个过程,除此之外,一般还需要编写一篇毕业论文。毕业论文有严格的格式要求和内容要求,但是我们从没写过毕业论文,那应该怎么达到要求呢?在这个时候,学院或指导老师都会给我们一份毕业论文模板,这个模板将告诉我们在什么位置用什么样的格式书写什么样的内容。

以我本科软件工程毕业论文而言,按论文内容来说,主要由引言、需求分析、系统概要设计、系统详细设计与实现、系统运行及测试、结论、参考文献、致谢和声明共八个部分组成。由于每个人选择的毕业设计题目的不同,所编写毕业论文的具体内容也不尽相同,但论文整体都是由以上八部分组成。

在比如最近几篇设计模式相关的文章,我都是按照模式的定义与特点、适用场景、模式的结构、代码的实现和模式的拓展共 5 个部分进行编写的。

咱们将这种思想运用到编程中:比如我们在设计一个系统时知道了其算法所需的步骤和这些步骤的执行顺序,但对于某些步骤的具体实现还未考虑,或者说某些步骤的实现与具体的环境相关。那么我们可以定义一个父类,这个父类里编写多个抽象方法,这些抽象方法代表了算法步骤的抽象,除此之外还要一个编排方法,这个编排方法指定了这些步骤的执行顺序。最后只需要在开发过程中继承或实现这个父类,重写其中的抽象方法即可完成算法的编写。

这种方式就是【模板方法模式】的主要内容。

这时候有人就会问了:这样抽象的意义何在呢?我直接编写算法具体的实现不也一样吗?

确实,从我上面举的例来说的确还不如直接编写具体的实现。那如果算法中某些步骤容易发生改变呢?这个时候抽象的意义不就体现出来了?当然这只是【模板方法模式】的一个使用场景,它主要解决一些方法通用,却在每一个子类都重新写了这一方法。

定义

模板方法(Template Method)模式:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。它是一种类行为型模式。

优点

1、它封装了不变部分,扩展可变部分。它把认为是不变部分的算法封装到父类中实现,而把可变部分算法由子类继承实现,便于子类继续扩展。

2、它在父类中提取了公共的部分代码,便于代码复用和维护。

3、行为由父类控制,部分方法由子类实现,因此子类可以通过扩展方式增加相应的功能,符合开闭原则。

缺点

1、对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象,间接地增加了系统实现的复杂度。

2、父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。

3、由于继承关系自身的缺点,如果父类添加新的抽象方法,则所有子类都要改一遍。

2. 适用场景

1、算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。

2、当多个子类存在公共的行为时,可以将其提取出来并集中到一个公共父类中以避免代码重复。首先,要识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。

3、当需要控制子类的扩展时,模板方法只在特定点调用钩子操作,这样就只允许在这些点进行扩展。

4、复杂的、重要的方法,可以考虑作为抽象模板中的模板方法。

在举一个应用实例的例子:Spring 中对 Hibernate 的支持,将一些已经定好的方法封装起来,比如开启事务、获取 Session、关闭 Session 等,程序员不重复写那些已经规范好的代码,直接丢一个实体就可以保存。

3. 模式的结构

【模板方法模式】包含抽象模板和具体子类两个角色。

1、抽象类 / 抽象模板(Abstract Class)

抽象模板类,负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。这些方法的定义如下:

1)模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。 为防止恶意操作,一般模板方法都加上 final 关键词。

2)基本方法:是整个算法中的一个步骤,包含以下几种类型:

① 抽象方法:在抽象类中声明,由具体子类实现。

② 具体方法:在抽象类中已经实现,在具体子类中可以继承或重写它。

③ 钩子方法:在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。

2、具体子类 / 具体实现(Concrete Class)

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

【模板方法模式】的结构图:

模板方法模式的结构图

相比于前面介绍的几种设计模式,【模板方法模式】的结构图相当简单,除去客户类,只包含两个类。

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
/**
* @author mofan
* @date 2021/9/4 17:44
*/
public abstract class AbstractClass {
/**
* 模板方法
* 使用 final 修饰,使子类无法重写
*/
public final void templateMethod() {
specificMethod();
abstractMethod1();
abstractMethod2();
}

/**
* 具体方法
* 在父类中就有具体的实现
*/
public void specificMethod() {
System.out.println("抽象类中的具体方法被调用...");
}

/**
* 抽象方法 1
*/
public abstract void abstractMethod1();

/**
* 抽象方法 2
*/
public abstract void abstractMethod2();
}

然后是抽象类的子类,含有抽象方法具体的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author mofan
* @date 2021/9/4 17:47
*/
public class ConcreteClass extends AbstractClass {
@Override
public void abstractMethod1() {
System.out.println("抽象方法 1 的实现被调用...");
}

@Override
public void abstractMethod2() {
System.out.println("抽象方法 2 的实现被调用...");
}
}

最后编写一个测试类测试一下:

1
2
3
4
5
@Test
public void testTemplate() {
AbstractClass cl = new ConcreteClass();
cl.templateMethod();
}

运行上述测试类后,控制台打印出:

抽象类中的具体方法被调用...
抽象方法 1 的实现被调用...
抽象方法 2 的实现被调用...

5. 模式的拓展

在介绍抽象模板类的组成时,其主要包括模板方法和基本方法,而基本方法又包含抽象方法、具体方法和钩子方法。在前面代码的实现中,抽象模板类中已经含有了抽象方法和具体方法,但是还没有钩子方法,正确使用钩子方法可以使得子类控制父类的行为。

比如下图所示的结构图中,抽象模板方法就含有钩子方法:

含钩子方法的模板方法模式的结构图

按照上述的结构图,不难得出含有钩子方法的【模板方法模式】的代码实现。首先是含有钩子方法的抽象模板类:

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
/**
* 含有钩子方法的抽象模板类
*
* @author mofan
* @date 2021/9/4 18:08
*/
public abstract class HookAbstractClass {
/**
* 模板方法
* 指定方法的执行顺序
*/
public final void templateMethod() {
abstractMethod1();
hookMethod1();
if (hookMethod2()) {
specificMethod();
}
abstractMethod2();
}

/**
* 具体方法
*/
public void specificMethod() {
System.out.println("抽象类中的具体方法被调用...");
}

/**
* 钩子方法 1
*/
public void hookMethod1() {
}

/**
* 钩子方法2
*
* @return true or false
*/
public boolean hookMethod2() {
return true;
}

/**
* 抽象方法 1
*/
public abstract void abstractMethod1();

/**
* 抽象方法 2
*/
public abstract void abstractMethod2();
}

然后是含有钩子方法的具体子类:

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
/**
* 含有钩子方法的具体子类
*
* @author mofan
* @date 2021/9/4 18:10
*/
public class HookConcreteClass extends HookAbstractClass {
@Override
public void abstractMethod1() {
System.out.println("抽象方法 1 的实现被调用...");
}

@Override
public void abstractMethod2() {
System.out.println("抽象方法 2 的实现被调用...");
}

@Override
public void hookMethod1() {
System.out.println("钩子方法 1 被重写...");
}

@Override
public boolean hookMethod2() {
return false;
}
}

最后依旧是一个测试方法:

1
2
3
4
5
@Test
public void testHookTemplate() {
HookAbstractClass hcl = new HookConcreteClass();
hcl.templateMethod();
}

运行上述测试方法后,控制台打印出:

抽象方法 1 的实现被调用...
钩子方法 1 被重写...
抽象方法 2 的实现被调用...

如果在具体子类中的钩子方法 hookMethod1()hookMethod2() 的代码发生改变,那么程序的运行结果也会发生改变。