模板方法模式

设计模式笔记:模板方法模式 (Template Method Pattern)

一、一句话概括

在一个方法中定义一个算法的骨架,而将一些具体的步骤延迟到子类中去实现。这使得子类可以在不改变算法整体结构的情况下,重新定义算法中的某些特定步骤。

二、为什么需要它 (Why - The Pain Point)

想象一下,你正在开发一个泡制饮料的程序。你需要支持泡咖啡(Coffee)和泡茶(Tea)。我们来分析一下它们的制作流程:

泡咖啡(prepareCoffee()):

  1. 把水烧开 (boilWater())
  2. 用沸水冲泡咖啡 (brewCoffeeGrinds())
  3. 把咖啡倒进杯子 (pourInCup())
  4. 加糖和牛奶 (addSugarAndMilk())

泡茶(prepareTea()):

  1. 把水烧开 (boilWater())
  2. 用沸水浸泡茶叶 (steepTeaBag())
  3. 把茶倒进杯子 (pourInCup())
  4. 加柠檬 (addLemon())

痛点:代码重复和流程僵化

如果你为 CoffeeTea 分别编写代码,你会发现:

  • 代码重复:步骤1(烧水)和步骤3(倒进杯子)是完全一样的。代码重复是万恶之源。
  • 流程不统一:虽然大体流程相似,但没有一个统一的、强制性的算法框架来约束它们。如果未来新增一种饮料,开发者可能会忘记某个步骤,或者打乱步骤的顺序。

核心问题:如何在一个流程或算法中,既能复用公共的部分,又能方便地定制变化的部分,同时还能保证整个流程的结构稳定不变?

三、它是什么 (What - The Solution)

模板方法模式通过继承来解决这个问题。它定义了一个“模板方法”,这个方法位于抽象父类中,它封装了整个算法的固定结构。

核心思想和角色:

  1. **抽象类 (Abstract Class)**:

    • 它包含一个模板方法(Template Method)。这个方法是 publicfinal 的,以防止子类重写它,从而保证算法结构的稳定性。
    • 模板方法内部按顺序调用了一系列“步骤方法”。
    • 这些步骤方法可以是:
      • 抽象方法 (Abstract Method):必须由子类实现。这些是算法中必须变化的部分。例如,brew()(冲泡原料)。
      • 具体方法 (Concrete Method):父类提供默认实现,子类可以选择性地重写。这些是算法中可能变化的部分。
      • **钩子 (Hook)**:这是一种特殊的具体方法。它通常在父类中有一个空的实现,或者返回一个默认值。子类可以通过重写钩子来“挂入”算法流程的某个特定点,以影响算法的行为。例如,一个 customerWantsCondiments()(顾客是否要加调料)的钩子,如果子类重写它返回 false,模板方法就可以跳过“加调料”这一步。
  2. **具体类 (Concrete Class)**:

    • 继承自抽象类。
    • 实现了父类中所有的抽象方法。
    • 根据需要重写父类中的具体方法或钩子。
    • 它不需要(也不能)重写模板方法。

好莱坞原则 (Hollywood Principle): “Don’t call us, we’ll call you.”
模板方法模式完美地体现了这一原则。父类(框架)控制着整个算法的流程,它在需要的时候会“调用”子类(用户代码)实现的具体步骤,而不是让子类来驱动整个流程。这是一种控制反转(Inversion of Control, IoC) 的体现。

四、怎么做 (How - The Blueprint)

基本步骤(以泡饮料为例):

  1. **创建抽象类 CaffeineBeverage**。
  2. CaffeineBeverage 中,定义一个 final 的模板方法 prepareRecipe()
  3. prepareRecipe() 中,定义算法的骨架:
    1
    2
    3
    4
    5
    6
    boilWater();
    brew(); // 变化的步骤
    pourInCup();
    if (customerWantsCondiments()) { // 钩子
    addCondiments(); // 变化的步骤
    }
  4. 将公共步骤 boilWater()pourInCup() 实现为具体方法。
  5. 将变化的步骤 brew()addCondiments() 声明为抽象方法。
  6. customerWantsCondiments() 实现为一个钩子(默认返回 true)。
  7. 创建具体子类 CoffeeTea,继承 CaffeineBeverage,并实现 brew()addCondiments() 方法。Tea 还可以重写钩子 customerWantsCondiments() 来控制是否加柠檬。

Java 示例代码

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
// 1. 抽象类 (AbstractClass)
abstract class CaffeineBeverage {
// 2. 模板方法,用 final 确保子类不能覆盖
public final void prepareRecipe() {
boilWater();
brew();
pourInCup();
if (customerWantsCondiments()) { // 钩子
addCondiments();
}
}

// 3. 抽象步骤,由子类实现
protected abstract void brew();
protected abstract void addCondiments();

// 4. 具体步骤,所有子类通用
private void boilWater() {
System.out.println("Boiling water");
}

private void pourInCup() {
System.out.println("Pouring into cup");
}

// 5. 钩子 (Hook),子类可以覆盖它来控制流程
protected boolean customerWantsCondiments() {
return true; // 默认需要调料
}
}

// 6. 具体类 (ConcreteClass) - Coffee
class Coffee extends CaffeineBeverage {
@Override
protected void brew() {
System.out.println("Dripping coffee through filter");
}

@Override
protected void addCondiments() {
System.out.println("Adding sugar and milk");
}
}

// 6. 具体类 (ConcreteClass) - Tea
class Tea extends CaffeineBeverage {
@Override
protected void brew() {
System.out.println("Steeping the tea bag");
}

@Override
protected void addCondiments() {
System.out.println("Adding lemon");
}

// 重写钩子,可以改变算法流程
@Override
protected boolean customerWantsCondiments() {
// 模拟询问用户
// String answer = getUserInput();
// return answer.toLowerCase().startsWith("y");
return false; // 假设这次用户不想要调料
}
}

// 客户端
public class TemplateMethodPatternDemo {
public static void main(String[] args) {
System.out.println("--- Making Coffee ---");
CaffeineBeverage coffee = new Coffee();
coffee.prepareRecipe();

System.out.println("\n--- Making Tea ---");
CaffeineBeverage tea = new Tea();
tea.prepareRecipe();
}
}

五、UML 类图

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
@startuml
skinparam classAttributeIconSize 0
hide empty members

abstract class CaffeineBeverage {
' Template Method '
+ final void prepareRecipe()
# {abstract} void brew()
# {abstract} void addCondiments()
- void boilWater()
- void pourInCup()
# boolean customerWantsCondiments()
}

class Coffee extends CaffeineBeverage {
# void brew()
# void addCondiments()
}

class Tea extends CaffeineBeverage {
# void brew()
# void addCondiments()
# boolean customerWantsCondiments()
}

class TemplateMethodPatternDemo {}
TemplateMethodPatternDemo ..> CaffeineBeverage : uses

@enduml

六、优缺点分析

优点 (Pros):

  1. 代码复用:将公共代码提取到父类中,避免了代码重复。
  2. 封装不变部分,扩展可变部分:算法的骨架(不变部分)被固定在父类,而具体实现(可变部分)则委托给子类,非常符合开闭原则。
  3. 框架化:这是构建框架的常用技术。框架定义了主要的控制流程,开发者只需继承并填充特定的业务逻辑即可。
  4. 控制反转:父类调用子类的操作,通过这种反向控制,确保了算法结构的统一性。

缺点 (Cons):

  1. 继承的限制:模板方法模式是基于继承的,这带来了一些固有的限制。比如子类必须继承父类,这在某些情况下可能不方便。
  2. 可读性可能降低:如果父类的模板方法过于复杂,或者钩子方法过多,可能会让子类的实现者难以理解算法的完整流程。

七、在 Java 中的应用

模板方法模式在 Java JDK 和各种框架中被广泛使用。

  • java.io.InputStream: read(byte[] b) 方法内部调用了抽象的 read() 方法,这是一个典型的模板方法。子类如 FileInputStream 只需要实现基本的 read(),更高效的 read(byte[] b) 的逻辑已经由父类提供了。
  • java.util.AbstractList: 它是 List 接口的一个抽象实现。它实现了 add, remove 等方法,但这些方法内部依赖于像 get(int index), set(int index, E element) 这样的抽象方法。如果你想创建一个自己的 List 实现,只需继承 AbstractList 并实现这些核心的抽象方法,就能免费获得大部分 List 功能。
  • Java Servlet 框架: javax.servlet.http.HttpServlet 类中的 service() 方法就是一个模板方法。它根据 HTTP 请求的类型(GET, POST 等)来调用具体的 doGet(), doPost() 等方法。开发者只需继承 HttpServlet 并重写 doGet()doPost(),而无需关心 service() 方法中复杂的请求分派逻辑。
  • Spring 框架: JdbcTemplate, RestTemplate 等模板类中大量使用了该模式(通常与回调函数结合),来封装资源管理、异常处理等固定流程。