什么是模板方法模式?应用场景有哪些?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

面试官提出这个问题,通常旨在考察以下几个层面的能力:

  1. 对设计模式的认知广度与深度:确认你是否系统学习过常用的设计模式,而不仅仅是死记硬背概念。
  2. 对模板方法模式的本质理解
    • 不仅仅是知道定义,更想知道你是否理解其 “定义算法骨架,允许子类重写特定步骤” 的核心思想。
    • 能否清晰区分 “模板方法” 与 “基本方法”(抽象方法、具体方法、钩子方法)。
  3. 实际应用与抽象思维能力
    • 能否识别出现有框架或项目中模板方法模式的应用,这体现了你的代码阅读经验和框架理解能力。
    • 能否在合适的业务场景中主动应用该模式,解决 “代码重复” 和 “流程固化” 的问题。
  4. 对面向对象设计原则的把握:该模式是 “开闭原则”(对扩展开放,对修改封闭)和 “里氏替换原则” 的经典体现,面试官会间接考察你对这些原则的理解。

核心答案

模板方法模式 是一种行为设计模式。它在父类(抽象类)中定义一个操作中的算法骨架,而将一些步骤的具体实现延迟到子类中。模板方法使得子类可以在不改变算法整体结构的情况下,重新定义该算法的某些特定步骤。

核心就两点1. 固定流程;2. 可变实现

其典型的应用场景包括:

  1. 框架设计:各类框架(如 Spring, JUnit)中大量使用,用于固定核心流程,暴露扩展点给用户。
  2. 具有统一流程的业务逻辑:如订单处理(校验 -> 计价 -> 库存锁定 -> 持久化)、数据导出(准备数据 -> 生成文件 -> 上传到指定位置)等。
  3. 工具类与算法库:提供通用的算法框架,允许调用者定制部分逻辑。

深度解析

原理/机制

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

  • 抽象类(Abstract Class)
    • 模板方法:一个 final 方法,定义了算法的骨架和步骤执行顺序。它通常会调用下列各类 “基本方法”。
    • 基本方法
      • 抽象方法:由子类必须实现的方法,对应算法中可变的步骤。
      • 具体方法:在抽象类中已实现的方法,对应算法中固定的步骤,子类通常无需关心。
      • 钩子方法:在抽象类中提供默认(通常为空)实现的方法。子类可以选择性地覆盖它,从而影响模板方法的流程。这是一种更细粒度的控制。
  • 具体子类(Concrete Class):实现父类中定义的抽象方法,并可选择性地覆盖钩子方法。

该模式通过将不变的行为(流程)搬移到父类,去除子类中的重复代码,实现了良好的复用性,同时提供了足够的扩展灵活性。

代码示例

以“饮料冲泡”这个经典例子为例,泡咖啡和泡茶的流程固定,但某些步骤具体操作不同。

// 1. 抽象类:定义模板和基本方法
public abstract class BeverageTemplate {

    // 模板方法:声明为 final,防止子类重写算法骨架
    public final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        // 钩子方法控制是否添加调料
        if (customerWantsCondiments()) {
            addCondiments();
        }
    }

    // 抽象方法:必须由子类实现
    protected abstract void brew();
    protected abstract void addCondiments();

    // 具体方法:通用步骤,已实现
    private void boilWater() {
        System.out.println("将水煮沸");
    }
    private void pourInCup() {
        System.out.println("倒入杯中");
    }

    // 钩子方法:子类可选择覆盖,提供默认实现
    protected boolean customerWantsCondiments() {
        return true; // 默认需要加调料
    }
}

// 2. 具体子类:实现特定步骤
public class Coffee extends BeverageTemplate {
    @Override
    protected void brew() {
        System.out.println("用沸水冲泡咖啡粉");
    }
    @Override
    protected void addCondiments() {
        System.out.println("加入糖和牛奶");
    }
    // 覆盖钩子方法:提供个性化逻辑
    @Override
    protected boolean customerWantsCondiments() {
        // 假设通过某种方式获取用户偏好,这里简化为固定返回 false
        return false; // 这位客人喝黑咖啡
    }
}

public class Tea extends BeverageTemplate {
    @Override
    protected void brew() {
        System.out.println("用沸水浸泡茶叶");
    }
    @Override
    protected void addCondiments() {
        System.out.println("加入柠檬");
    }
}

// 3. 客户端使用
public class Client {
    public static void main(String[] args) {
        System.out.println("制作咖啡...");
        BeverageTemplate coffee = new Coffee();
        coffee.prepareRecipe(); // 执行固定流程,调用子类实现的特定步骤

        System.out.println("\n制作茶...");
        BeverageTemplate tea = new Tea();
        tea.prepareRecipe();
    }
}

对比分析与注意事项

  • 与策略模式对比:两者都用于封装算法。关键区别在于:

    • 模板方法模式 使用继承,通过子类来改变部分算法。它强调算法步骤的固定
    • 策略模式 使用组合,通过注入不同的策略对象来改变整个算法。它强调算法的完全可互换
    • 简单来说,模板方法是 “一部分变,一部分不变”;策略模式是 “整个都变,我提供几套完整方案给你选”

    最佳实践与常见误区

    • 保护模板方法:模板方法 (prepareRecipe) 应声明为 final,防止子类重写从而破坏算法结构。
    • 尽量减少抽象方法:不是所有步骤都需要是抽象的。将稳定的步骤实现为具体方法,减少子类的负担。
    • 合理使用钩子方法:钩子方法提供了额外的控制点,但不宜过多,否则会提高系统复杂度,让流程变得难以理解。
    • 警惕 “继承泛滥”:如果算法的可变部分过多,或者子类需要重写大量父类方法,可能意味着模板方法模式不适用,应考虑策略模式等基于组合的模式。
    • 命名约定:良好的命名能提高代码可读性,如模板方法名可为 doWork(), 钩子方法名常以 do..., will..., did..., is..., should... 等开头。

总结

模板方法模式的核心价值在于 “流程复用与扩展” ,它通过将不变的算法骨架提升至父类来消除代码重复,并通过抽象方法和钩子方法为子类提供灵活的扩展点,是框架设计中构筑 “骨架” 的基石。