基于本文回答
0
评论

设计模式--模板方法模式 (Template Method)

知识点图片

模板方法模式是一种行为设计模式,它在父类中定义一个算法的骨架,将可变步骤延迟到子类实现。这实现了代码复用并固定了核心流程,核心是“定义骨架,延迟实现”。

我们来详细、清晰地讲解设计模式中的模板方法模式 (Template Method Pattern)

1. 引言:一个生活中的例子

想象一下你去一家自助饮品店制作一杯饮料。无论是制作咖啡还是奶茶,基本流程都是固定的:

  1. 烧开水 (Boil Water)
  2. 冲泡主料 (Brew) - 咖啡是冲泡咖啡粉,奶茶是冲泡茶叶。
  3. 倒入杯中 (Pour into Cup)
  4. 加入调料 (Add Condiments) - 咖啡加糖和奶,奶茶加珍珠和糖。

在这个过程中,步骤 1(烧水)和步骤 3(倒杯)是完全一样的,是固定不变的。而步骤 2(冲泡主料)和步骤 4(加调料)则是可变的,具体做什么取决于你最终想要的是咖啡还是奶茶。

模板方法模式就是用来解决这类问题的:一个算法的流程(骨架)是固定的,但其中某些步骤的具体实现可能不同。


2. 什么是模板方法模式?

模板方法模式 (Template Method Pattern) 是一种行为设计模式。它在一个方法中定义一个算法的骨架,而将一些步骤的实现延迟到子类中。模板方法使得子类可以不改变一个算法的结构,就能重新定义该算法的某些特定步骤。

核心思想: 将不变的部分(算法流程)封装在父类中,将可变的部分(具体步骤)延迟到子类中实现。这完美体现了“好莱坞原则”——“别来找我们,我们会来找你”(Don't call us, we'll call you)。父类(框架)调用子类(具体实现)的方法,而不是反过来。


3. 模式的结构

模板方法模式的结构非常简单,主要包含两个角色:

  1. 抽象类 (AbstractClass):

    • 负责定义模板方法 (Template Method),这个方法定义了算法的骨架,它会按照特定顺序调用其他方法。
    • 定义一个或多个基本方法 (Primitive Methods),这些方法可以被模板方法调用。
    • 基本方法可以是:
      • 抽象方法 (Abstract Methods): 必须由子类实现。
      • 具体方法 (Concrete Methods): 父类提供默认实现,子类可以覆盖(override)。
      • 钩子方法 (Hook Methods): 父类提供一个默认(通常是空的)实现。子类可以选择性地覆盖它,用来在算法的特定点“挂钩”上额外的行为,或者控制算法的流程(例如,一个返回 boolean 的钩子可以决定某个步骤是否执行)。
  2. 具体类 (ConcreteClass):

    • 继承抽象类。
    • 实现父类中定义的抽象方法,或者覆盖父类中已有的具体方法,以完成算法中与自身相关的步骤。

UML 结构图

plaintext
+---------------------------+
|       AbstractClass       |
+---------------------------+
|                           |
| + final templateMethod()  |  <-- 模板方法,定义算法骨架
| # primitiveOperation1()   |  <-- 抽象方法,子类必须实现
| # primitiveOperation2()   |  <-- 抽象方法,子类必须实现
| # hookMethod()            |  <-- 钩子方法,子类可选实现
+---------------------------+
          ^
          | (is-a)
+---------------------------+
|       ConcreteClass       |
+---------------------------+
|                           |
| # primitiveOperation1()   |  <-- 实现步骤1
| # primitiveOperation2()   |  <-- 实现步骤2
| # hookMethod()            |  <-- 可选地覆盖钩子
+---------------------------+

4. 代码示例 (Java)

我们用上面制作饮料的例子来实现模板方法模式。

步骤 1: 创建抽象类 BeverageMaker

这个类定义了制作饮料的整个流程(模板方法 makeBeverage()),并声明了需要子类实现的具体步骤。

java
// 抽象类:饮料制作器
public abstract class BeverageMaker {

    // 模板方法,定义了制作饮料的完整流程
    // 使用 final 关键字,防止子类修改算法的骨架
    public final void makeBeverage() {
        boilWater();
        brew();
        pourInCup();
        if (customerWantsCondiments()) { // 使用钩子方法
            addCondiments();
        }
    }

    // 基本方法 - 具体方法 (所有子类共用)
    private void boilWater() {
        System.out.println("1. 烧开水");
    }

    private void pourInCup() {
        System.out.println("3. 将饮料倒入杯中");
    }

    // 基本方法 - 抽象方法 (子类必须实现)
    protected abstract void brew();
    protected abstract void addCondiments();

    // 钩子方法 (Hook Method)
    // 子类可以选择是否覆盖它,来决定是否需要调料
    protected boolean customerWantsCondiments() {
        return true; // 默认需要调料
    }
}

步骤 2: 创建具体类 CoffeeTea

这两个类继承 BeverageMaker 并实现自己的特定步骤。

java
// 具体类:咖啡制作器
public class Coffee extends BeverageMaker {
    @Override
    protected void brew() {
        System.out.println("2. 用沸水冲泡咖啡粉");
    }

    @Override
    protected void addCondiments() {
        System.out.println("4. 加糖和牛奶");
    }
}

// 具体类:茶制作器
public class Tea extends BeverageMaker {
    @Override
    protected void brew() {
        System.out.println("2. 用沸水浸泡茶叶");
    }

    @Override
    protected void addCondiments() {
        System.out.println("4. 加柠檬");
    }
    
    // 覆盖钩子方法,假设我们有一种清茶,默认不加调料
    @Override
    protected boolean customerWantsCondiments() {
        // 可以在这里加入更复杂的逻辑,比如询问用户
        System.out.println("顾客不需要任何调料。");
        return false;
    }
}

步骤 3: 客户端代码

客户端代码只需要和具体的子类交互,并调用统一的模板方法。

java
public class Client {
    public static void main(String[] args) {
        System.out.println("--- 制作一杯咖啡 ---");
        BeverageMaker coffee = new Coffee();
        coffee.makeBeverage();

        System.out.println("\n--- 制作一杯清茶(不加调料)---");
        BeverageMaker tea = new Tea();
        tea.makeBeverage();
    }
}

运行结果

plaintext
--- 制作一杯咖啡 ---
1. 烧开水
2. 用沸水冲泡咖啡粉
3. 将饮料倒入杯中
4. 加糖和牛奶

--- 制作一杯清茶(不加调料)---
1. 烧开水
2. 用沸水浸泡茶叶
3. 将饮料倒入杯中
顾客不需要任何调料。

从结果可以看出,我们调用了相同的 makeBeverage() 方法,但由于具体实现类的不同,最终的行为也不同。算法的整体结构被父类牢牢控制,而变化的细节则由子类自由发挥。


5. 优缺点

优点

  1. 代码复用: 将所有子类中公共的行为提取到父类,避免了代码重复。
  2. 封装不变部分,扩展可变部分: 将算法的骨架固定在父类,易于维护。同时通过子类来扩展新的行为,符合“开闭原则”。
  3. 行为控制: 父类可以控制子类的行为范围,模板方法通常被声明为 final,确保子类无法修改算法的整体流程。

缺点

  1. 继承的局限性: 模板方法模式是基于继承的,这导致子类和父类之间存在较强的耦合。如果父类的算法骨架发生变化,可能会影响到所有子类。
  2. 类的数量增加: 每一种新的算法变体都需要一个新的具体子类,这可能会导致系统中类的数量增多。

6. 适用场景

  1. 算法的关键步骤固定,但具体实现可变: 当多个类有相同的一系列步骤,但某些步骤的实现细节不同时。例如,各种数据加载器(从文件、数据库、网络加载),其“打开->读取->解析->关闭”的流程是固定的,但“读取”和“解析”的实现不同。
  2. 控制子类的扩展: 当你想控制子类扩展的范围,只允许它们在特定的扩展点进行修改时。
  3. 框架和库的设计: 模板方法模式在框架设计中非常常见。框架定义了处理流程,而开发者只需要继承框架中的类并实现特定的“钩子”或“抽象”方法,就能将自己的业务逻辑嵌入到框架中。例如,Java Servlet 中的 doGetdoPost 方法,JUnit 中的 setUptearDown 方法等。

7. 与其他模式的比较

模板方法模式 vs. 策略模式 (Strategy Pattern)

这是最常被比较的两个模式,因为它们都旨在封装算法,但实现方式不同。

特性 模板方法模式 (Template Method) 策略模式 (Strategy)
核心思想 基于继承 基于组合/委托
关系 "is-a" 关系(子类是一个抽象类的特殊化) "has-a" 关系(上下文持有一个策略对象)
粒度 改变算法的一部分 替换整个算法
灵活性 在编译时确定行为(通过子类) 可以在运行时动态改变行为(通过更换策略对象)
示例 制作饮料,流程固定,部分步骤可变 排序算法,Context 可以随时切换 BubbleSortQuickSort 等策略

简单总结:

  • 如果你想重构一个算法的部分步骤,并且这种变化是静态的,使用模板方法模式
  • 如果你想让一个类能够动态地切换整个算法,并且希望算法之间可以完全独立,使用策略模式

模板方法模式 vs. 工厂方法模式 (Factory Method Pattern)

工厂方法模式可以看作是模板方法模式的一个特殊应用。在工厂方法中,创建对象的逻辑被推迟到子类。一个包含工厂方法的类通常也有一个模板方法,该方法定义了创建和使用对象的流程,而具体的对象创建步骤(即工厂方法本身)则由子类实现。

总结

模板方法模式是一种简单而强大的模式,它通过继承机制将算法的固定结构和可变实现分离开来,是实现代码复用和框架设计的基石。它的核心在于“定义骨架,延迟实现”

右滑查看面试常问