模板方法模式
# 介绍
板方法模式是一种行为设计模式, 它在超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。
# 背景
假如你正在开发一款分析公司文档的数据挖掘程序。 用户需要向程序输入各种格式 (PDF、 DOC 或 CSV) 的文档, 程序则会试图从这些文件中抽取有意义的数据, 并以统一的格式将其返回给用户。
该程序的首个版本仅支持 DOC 文件。 在接下来的一个版本中, 程序能够支持 CSV 文件。 一个月后, 你 “教会” 了程序从 PDF 文件中抽取数据。
一段时间后, 你发现这三个类中包含许多相似代码。 尽管这些类处理不同数据格式的代码完全不同, 但数据处理和分析的代码却几乎完全一样。 如果能在保持算法结构完整的情况下去除重复代码, 这难道不是一件很棒的事情吗?
还有另一个与使用这些类的客户端代码相关的问题: 客户端代码中包含许多条件语句, 以根据不同的处理对象类型选择合适的处理过程。 如果所有处理数据的类都拥有相同的接口或基类, 那么你就可以去除客户端代码中的条件语句, 转而使用多态机制来在处理对象上调用函数。
模板方法模式建议将算法分解为一系列步骤, 然后将这些步骤改写为方法, 最后在 “模板方法” 中依次调用这些方法。 步骤可以是 抽象
的, 也可以有一些默认的实现。 为了能够使用算法, 客户端需要自行提供子类并实现所有的抽象步骤。 如有必要还需重写一些步骤 (但这一步中不包括模板方法自身)。
让我们考虑如何在数据挖掘应用中实现上述方案。 我们可为图中的三个解析算法创建一个基类, 该类将定义调用了一系列不同文档处理步骤的模板方法。
模板方法将算法分解为步骤, 并允许子类重写这些步骤, 而非重写实际的模板方法。
首先, 我们将所有步骤声明为 抽象
类型, 强制要求子类自行实现这些方法。 在我们的例子中, 子类中已有所有必要的实现, 因此我们只需调整这些方法的签名, 使之与超类的方法匹配即可。
现在, 让我们看看如何去除重复代码。 对于不同的数据格式, 打开和关闭文件以及抽取和解析数据的代码都不同, 因此无需修改这些方法。 但分析原始数据和生成报告等其他步骤的实现方式非常相似, 因此可将其提取到基类中, 以让子类共享这些代码。
正如你所看到的那样, 我们有两种类型的步骤:
- 抽象步骤必须由各个子类来实现
- 可选步骤已有一些默认实现, 但仍可在需要时进行重写
还有另一种名为钩子的步骤。 钩子是内容为空的可选步骤。 即使不重写钩子, 模板方法也能工作。 钩子通常放置在算法重要步骤的前后, 为子类提供额外的算法扩展点。
# 结构
- 抽象类 (AbstractClass) 会声明作为算法步骤的方法, 以及依次调用它们的实际模板方法。 算法步骤可以被声明为
抽象
类型, 也可以提供一些默认实现。 - 具体类 (ConcreteClass) 可以重写所有步骤, 但不能重写模板方法自身。
# 代码
假设我们有一个制作饮料的模板,其中包含了一些共同的步骤,如煮水、冲泡、加入调料等。但每种饮料的具体调料和冲泡方式可能不同。这时,我们可以使用模板方法模式来定义这个制作饮料的过程:
// 抽象类:制作饮料的模板
abstract class BeverageMaker {
// 模板方法,定义制作饮料的算法骨架
public final void makeBeverage() {
boilWater();
brew();
addCondiments();
// 可选的钩子方法,子类可以选择是否覆盖它
if (customerWantsCondiments()) {
addExtraCondiments();
}
}
// 具体步骤由子类实现
protected abstract void brew();
protected abstract void addCondiments();
// 钩子方法,提供默认实现,子类可以选择是否覆盖它
protected boolean customerWantsCondiments() {
return true;
}
// 公共步骤
private void boilWater() {
System.out.println("Boiling water");
}
// 可选步骤
protected void addExtraCondiments() {
System.out.println("Adding extra condiments");
}
}
// 具体子类:制作茶
class TeaMaker extends BeverageMaker {
@Override
protected void brew() {
System.out.println("Steeping tea");
}
@Override
protected void addCondiments() {
System.out.println("Adding lemon");
}
}
// 具体子类:制作咖啡
class CoffeeMaker extends BeverageMaker {
@Override
protected void brew() {
System.out.println("Dripping coffee through filter");
}
@Override
protected void addCondiments() {
System.out.println("Adding sugar and milk");
}
// 覆盖钩子方法
@Override
protected boolean customerWantsCondiments() {
return false; // 不加额外调料
}
}
public class Main {
public static void main(String[] args) {
BeverageMaker teaMaker = new TeaMaker();
teaMaker.makeBeverage();
BeverageMaker coffeeMaker = new CoffeeMaker();
coffeeMaker.makeBeverage();
}
}
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
# 模板方法模式适合应用场景
当你只希望客户端扩展某个特定算法步骤, 而不是整个算法或其结构时, 可使用模板方法模式。
模板方法将整个算法转换为一系列独立的步骤, 以便子类能对其进行扩展, 同时还可让超类中所定义的结构保持完整。
当多个类的算法除一些细微不同之外几乎完全一样时, 你可使用该模式。 但其后果就是, 只要算法发生变化, 你就可能需要修改所有的类。
在将算法转换为模板方法时, 你可将相似的实现步骤提取到超类中以去除重复代码。 子类间各不同的代码可继续保留在子类中。
# 实现方式
- 分析目标算法, 确定能否将其分解为多个步骤。 从所有子类的角度出发, 考虑哪些步骤能够通用, 哪些步骤各不相同。
- 创建抽象基类并声明一个模板方法和代表算法步骤的一系列抽象方法。 在模板方法中根据算法结构依次调用相应步骤。 可用
final
最终修饰模板方法以防止子类对其进行重写。 - 虽然可将所有步骤全都设为抽象类型, 但默认实现可能会给部分步骤带来好处, 因为子类无需实现那些方法。
- 可考虑在算法的关键步骤之间添加钩子。
- 为每个算法变体新建一个具体子类, 它必须实现所有的抽象步骤, 也可以重写部分可选步骤。
# 模板方法模式优缺点
你可仅允许客户端重写一个大型算法中的特定部分, 使得算法其他部分修改对其所造成的影响减小。
你可将重复代码提取到一个超类中。
部分客户端可能会受到算法框架的限制。
通过子类抑制默认步骤实现可能会导致违反里氏替换原则。
模板方法中的步骤越多, 其维护工作就可能会越困难。
# spring中的模板方法
# 模板抽象类
AbstractPlatformTransactionManager
是Spring中的模板抽象类
,来看看它的继承关系图:
- 实现了
PlatformTransactionManager
接口,重载了接口中的方法。
# 模板方法
- 事务管理器中抽象类中的模板方法不止一个,比如以下两个方法
//提交事务
public final void commit()
//获取TransactionStatus
public final TransactionStatus getTransaction()
2
3
4
5
- 这两个方法都对于自己要实现的逻辑搭建了一个骨架,主要的功能是由抽象方法完成,由子类来完成。
# 抽象方法
- 事务管理器抽象类中的抽象方法定义了多个,分别用于处理不同的业务逻辑,由子类实现其中具体的逻辑,如下:
//提交事务
protected abstract void doCommit(DefaultTransactionStatus status);
//回滚事务
protected abstract void doRollback(DefaultTransactionStatus status);
//开始事务
protected abstract void doBegin(Object transaction, TransactionDefinition definition)
//获取当前的事务对象
protected abstract Object doGetTransaction()
2
3
4
5
6
7
8
9
10
11
- 抽象方法的定义便于子类去扩展,在保证算法逻辑不变的情况下,子类能够定制自己的实现。
# 具体子类
- 事务管理器的模板类有很多的具体子类,如下图:
- 其中我们熟悉的有
DataSourceTransactionManager
、JtaTransactionManager
、RabbitTransactionManager
。具体承担什么样的角色和责任不是本节的重点,不再细说。
# 总结
- 模板模式是一个很重要,易扩展的模式,提高了代码复用性,在Spring中有着广泛的应用,比如
JDBCTemplate
,AbstractPlatformTransactionManager
,这些实现都用到了模板模式。