设计模式之模板方法模式

设计模式——模板方法模式 是一种行为设计模式, 它在超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。但调用将以抽象类中定义的方式进行。建议将算法或者一个系统的某个功能分解为一系列步骤, 然后将这些步骤改写为方法, 最后在 “模板方法” 中依次调用这些方法。步骤可以是抽象的, 也可以有一些默认的实现。为了能够使用算法, 客户端需要自行提供子类并实现所有的抽象步骤。如有必要还需重写一些步骤 (但这一步中不包括模板方法自身)。抽象final必须可以工厂方法模板方法模板方法策略。 阅读详情

设计模式之模板方法模式

阅读指引:本文较长,可按需跳读。

  • 想快速理解 → 读「概念」「举例理解」「工作流程」;
  • 想看代码 → 读「代码示例」(含 UML、C++、Java、函数式轻量变体);
  • 想区分相似模式 → 读「模板方法模式与策略模式的对比」;
  • 想判断该不该用 → 读「优缺点」「一个反面例子」「应用场景」;
  • 准备落地 → 读「注意事项」与「模板方法模式不负责什么」。

概念

定义

模板方法模式(Template Method Pattern) 是一种行为型设计模式。它的核心思想是:

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

换句话说:当一件事的流程骨架是固定的,但其中若干步骤的具体做法可能因场景而异时,可以把骨架写在父类的模板方法里,把可变步骤声明为抽象方法或钩子方法,交给子类实现。

结构

模板方法模式通常有两个角色(外加一个客户端):

  1. AbstractClass 抽象类
    • 定义模板方法(templateMethod()),它把算法骨架固定下来,通常声明为 final,防止子类篡改流程。
    • 声明抽象方法(primitive operations),由子类实现,是流程中的可变步骤。
    • 提供具体方法,是流程中的固定步骤,子类可直接复用。
    • 可选提供钩子方法(hook),给子类一个"可选覆盖"的扩展点。
  2. ConcreteClass 具体类
    • 继承抽象类。
    • 实现抽象方法,完成可变步骤。
    • 按需覆盖钩子方法。
  3. Client 客户端
    • 创建具体类实例,调用模板方法。客户端不关心流程细节,只触发一次调用。

这里要特别区分三类方法:

方法类型谁实现是否可覆盖作用
模板方法抽象类通常 final,禁止覆盖定义算法骨架与步骤顺序
抽象方法子类必须覆盖流程中的可变步骤
具体方法抽象类可覆盖但不必须流程中的固定步骤,可复用
钩子方法抽象类可选覆盖扩展点,默认空实现或默认行为

举例理解

假设你要冲泡一杯饮品。不管是咖啡还是茶,流程骨架都差不多:

烧水 → 冲泡 → 倒入杯中 → 加调料

但"冲泡"和"加调料"这两步,咖啡和茶的具体做法不同:

  • 咖啡:用咖啡粉冲泡,加糖和牛奶;
  • 茶:用茶叶冲泡,加柠檬。

如果你用 if/else 写:

if (type == 咖啡) {
    烧水();
    用咖啡粉冲泡();
    倒入杯中();
    加糖和牛奶();
} else if (type == 茶) {
    烧水();
    用茶叶冲泡();
    倒入杯中();
    加柠檬();
}

烧水、倒杯这两步是重复的;新增一种饮品(比如可可)就要再加一个分支,流程骨架被分散在多个分支里,容易改漏。

模板方法模式的解决办法: 把流程骨架提到抽象类里,只把可变步骤延迟到子类。

  1. 抽象类:饮品

    abstract class Beverage {
        // 模板方法:固定流程骨架,禁止子类覆盖
        final void prepareRecipe() {
            boilWater();          // 固定步骤
            brew();               // 可变步骤(抽象)
            pourInCup();          // 固定步骤
            if (customerWantsCondiments()) {  // 钩子
                addCondiments();  // 可变步骤(抽象)
            }
        }
    
        abstract void brew();
        abstract void addCondiments();
    
        void boilWater() { ... }
        void pourInCup() { ... }
    
        // 钩子:默认需要加调料,子类可覆盖
        boolean customerWantsCondiments() { return true; }
    }
    
  2. 具体类:咖啡、茶

    class Coffee extends Beverage {
        void brew() { 用咖啡粉冲泡(); }
        void addCondiments() { 加糖和牛奶(); }
    }
    
    class Tea extends Beverage {
        void brew() { 用茶叶冲泡(); }
        void addCondiments() { 加柠檬(); }
        // 覆盖钩子:茶默认不加调料
        boolean customerWantsCondiments() { return false; }
    }
    
  3. 客户端

    Beverage coffee = new Coffee();
    coffee.prepareRecipe();   // 烧水 → 咖啡粉冲泡 → 倒杯 → 加糖和牛奶
    
    Beverage tea = new Tea();
    tea.prepareRecipe();      // 烧水 → 茶叶冲泡 → 倒杯(钩子返回 false,跳过加调料)
    

模板方法就像"泡面包装上的步骤图":撕开调料包 → 加开水 → 盖盖等 3 分钟 → 拌匀。步骤顺序是固定的,但"加什么料""等多久"你可以自己定。包装不会因为你换了口味就改步骤图,流程骨架始终不变。

对比一下策略模式:策略模式是"我挑一个工具用",整个算法可替换;模板方法是"流程骨架我定好,你只填可变的那几步",算法结构不变,只换步骤实现。


工作流程

模板方法模式的工作流程可以理解成:

  1. 先定一个流程骨架:比如"冲泡饮品"的流程是烧水 → 冲泡 → 倒杯 → 加调料。
  2. 把固定步骤写进抽象类:烧水、倒杯是固定的,直接在抽象类里实现。
  3. 把可变步骤声明为抽象方法:冲泡、加调料因饮品而异,声明为 abstract,强制子类实现。
  4. 按需留钩子:比如"要不要加调料"这种可选行为,给一个默认实现,子类可覆盖。
  5. 模板方法锁定顺序:把上述步骤按顺序调用,声明为 final,子类不能改流程。
  6. 子类只填可变步骤:咖啡、茶各自实现 brew() 和 addCondiments(),流程骨架一行都不用改。
  7. 客户端调用模板方法:coffee.prepareRecipe(),一次调用触发完整流程。

一句话概括:

父类定骨架 → 固定步骤父类写 → 可变步骤子类填 → 钩子留扩展 → 模板方法锁流程 → 客户端一次调用。

关键点:模板方法模式控制的是"流程骨架的复用",不是"算法的运行时替换"。 一个具体类实例调用模板方法时,走的就是它自己实现的那套步骤,不存在"执行到一半换另一个子类"的说法。


代码示例

UML

下面用"冲泡饮品"的例子,画出模板方法模式涉及的所有类,并标注关系。

extends

extends

creates / uses

creates / uses

calls prepareRecipe()

«abstract»

Beverage

+prepareRecipe() : void

#brew() : void

#addCondiments() : void

#boilWater() : void

#pourInCup() : void

#customerWantsCondiments() : boolean

Coffee

#brew() : void

#addCondiments() : void

Tea

#brew() : void

#addCondiments() : void

#customerWantsCondiments() : boolean

Client

+main(args : String[]) : void

类/接口角色在"冲泡饮品"里的含义
Beverage抽象类 AbstractClass定义冲泡饮品的流程骨架,固定步骤 + 抽象步骤 + 钩子
Coffee具体类 ConcreteClass实现咖啡的冲泡和加调料步骤
Tea具体类 ConcreteClass实现茶的冲泡和加调料步骤,并覆盖钩子决定不加调料
Client客户端 Client创建具体饮品并调用 prepareRecipe(),不关心流程细节

图中最重要的两条线:

  • 具体类 → 抽象类:继承关系,子类复用流程骨架、只填可变步骤;
  • Client → 抽象类:客户端面向 Beverage 编程,调用模板方法,不依赖具体子类的流程细节。

与策略模式不同,模板方法模式里没有"上下文持有接口并委托"这一层,算法骨架就在抽象类里,子类通过继承参与流程。

C++ 实现

// ============================================================
// 模板方法模式(Template Method Pattern)完整示例
// 场景:冲泡饮品,流程骨架固定,冲泡与加调料因饮品而异。
//
// 对应 UML:
//   Beverage   —— 抽象类,定义模板方法与基本操作
//   Coffee     —— 具体类,实现咖啡的可变步骤
//   Tea        —— 具体类,实现茶的可变步骤并覆盖钩子
//   Client     —— 客户端,调用模板方法
// ============================================================

#include <iostream>
#include <memory>
#include <string>

// ------------------------------------------------------------
// 1. 抽象类 AbstractClass:饮品
//
// 说明:
//   - prepareRecipe() 是模板方法,声明为 final(C++11 起可用),
//     防止子类改变流程骨架;
//   - brew() / addCondiments() 是抽象方法,子类必须实现;
//   - boilWater() / pourInCup() 是具体方法,固定步骤,子类可复用;
//   - customerWantsCondiments() 是钩子方法,默认返回 true,
//     子类可按需覆盖。
// ------------------------------------------------------------
class Beverage {
public:
    virtual ~Beverage() = default;

    // 模板方法:固定流程骨架,禁止子类覆盖。
    // 注意:模板方法本身通常不声明为 virtual,或声明为 final virtual。
    void prepareRecipe() const {
        boilWater();
        brew();
        pourInCup();
        // 钩子:由子类决定是否需要加调料。
        if (customerWantsCondiments()) {
            addCondiments();
        }
    }

protected:
    // 抽象方法:可变步骤,子类必须实现。
    virtual void brew() const = 0;
    virtual void addCondiments() const = 0;

    // 具体方法:固定步骤,子类可直接复用。
    void boilWater() const {
        std::cout << "  烧水..." << std::endl;
    }

    void pourInCup() const {
        std::cout << "  倒入杯中..." << std::endl;
    }

    // 钩子方法:默认需要加调料,子类可覆盖。
    virtual bool customerWantsCondiments() const {
        return true;
    }
};

// ------------------------------------------------------------
// 2. 具体类 ConcreteClass:咖啡
// ------------------------------------------------------------
class Coffee : public Beverage {
protected:
    void brew() const override {
        std::cout << "  用咖啡粉冲泡咖啡..." << std::endl;
    }

    void addCondiments() const override {
        std::cout << "  加糖和牛奶..." << std::endl;
    }
    // 不覆盖钩子,沿用默认 true(需要加调料)。
};

// ------------------------------------------------------------
// 3. 具体类 ConcreteClass:茶
// ------------------------------------------------------------
class Tea : public Beverage {
protected:
    void brew() const override {
        std::cout << "  用茶叶冲泡茶..." << std::endl;
    }

    void addCondiments() const override {
        std::cout << "  加柠檬..." << std::endl;
    }

    // 覆盖钩子:茶默认不加调料。
    bool customerWantsCondiments() const override {
        return false;
    }
};

// ------------------------------------------------------------
// 4. 客户端 Client
// ------------------------------------------------------------
int main() {
    std::cout << "========== 冲泡咖啡 ==========" << std::endl;
    std::unique_ptr<Beverage> coffee = std::make_unique<Coffee>();
    coffee->prepareRecipe();

    std::cout << "\n========== 冲泡茶 ==========" << std::endl;
    std::unique_ptr<Beverage> tea = std::make_unique<Tea>();
    tea->prepareRecipe();

    // 说明:模板方法模式不支持"运行时切换流程实现"。
    // 一个具体类实例调用模板方法时,走的就是它自己实现的那套步骤。
    // 如果需要运行时替换整个算法,应使用策略模式。

    return 0;
}

Java 实现

// ============================================================
// 模板方法模式(Template Method Pattern)完整示例(Java 版)
// ============================================================

// ------------------------------------------------------------
// 1. 抽象类 AbstractClass:饮品
// ------------------------------------------------------------
abstract class Beverage {

    // 模板方法:固定流程骨架,声明为 final,禁止子类覆盖。
    public final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        if (customerWantsCondiments()) {
            addCondiments();
        }
    }

    // 抽象方法:可变步骤,子类必须实现。
    protected abstract void brew();
    protected abstract void addCondiments();

    // 具体方法:固定步骤,子类可直接复用。
    protected void boilWater() {
        System.out.println("  烧水...");
    }

    protected void pourInCup() {
        System.out.println("  倒入杯中...");
    }

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

// ------------------------------------------------------------
// 2. 具体类:咖啡
// ------------------------------------------------------------
class Coffee extends Beverage {
    @Override
    protected void brew() {
        System.out.println("  用咖啡粉冲泡咖啡...");
    }

    @Override
    protected void addCondiments() {
        System.out.println("  加糖和牛奶...");
    }
    // 不覆盖钩子,沿用默认 true。
}

// ------------------------------------------------------------
// 3. 具体类:茶
// ------------------------------------------------------------
class Tea extends Beverage {
    @Override
    protected void brew() {
        System.out.println("  用茶叶冲泡茶...");
    }

    @Override
    protected void addCondiments() {
        System.out.println("  加柠檬...");
    }

    // 覆盖钩子:茶默认不加调料。
    @Override
    protected boolean customerWantsCondiments() {
        return false;
    }
}

// ------------------------------------------------------------
// 4. 客户端 Client
// ------------------------------------------------------------
public class TemplateMethodDemo {
    public static void main(String[] args) {
        System.out.println("========== 冲泡咖啡 ==========");
        Beverage coffee = new Coffee();
        coffee.prepareRecipe();

        System.out.println("\n========== 冲泡茶 ==========");
        Beverage tea = new Tea();
        tea.prepareRecipe();

        // 设计取舍:
        //   模板方法模式通过继承复用流程骨架,子类只填可变步骤。
        //   代价是子类与父类强耦合,且无法在运行时切换"流程实现"。
        //   如果业务需要运行时选择算法,应改用策略模式。
    }
}

函数式轻量变体

模板方法模式的核心是继承 + 流程骨架复用,这跟函数式风格天然不太合拍。但如果不追求"继承"这一形式,只想要"固定流程 + 可变步骤",可以用高阶函数 + 组合来模拟,本质上更接近"函数式编排"。

import java.util.function.Consumer;

// 用函数式接口表示"可变步骤"
// 这里用 Consumer<String> 模拟"对饮品名做操作"的步骤
class FnBeverage {
    private final Consumer<String> brew;
    private final Consumer<String> addCondiments;

    FnBeverage(Consumer<String> brew, Consumer<String> addCondiments) {
        this.brew = brew;
        this.addCondiments = addCondiments;
    }

    // 流程骨架:固定顺序,可变步骤以函数形式注入
    void prepareRecipe(String name) {
        boilWater();
        brew.accept(name);
        pourInCup();
        addCondiments.accept(name);
    }

    private void boilWater() {
        System.out.println("  烧水...");
    }

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

public class FnTemplateDemo {
    public static void main(String[] args) {
        FnBeverage coffee = new FnBeverage(
            name -> System.out.println("  用咖啡粉冲泡" + name + "..."),
            name -> System.out.println("  加糖和牛奶...")
        );
        FnBeverage tea = new FnBeverage(
            name -> System.out.println("  用茶叶冲泡" + name + "..."),
            name -> System.out.println("  加柠檬...")
        );

        System.out.println("========== 冲泡咖啡 ==========");
        coffee.prepareRecipe("咖啡");

        System.out.println("\n========== 冲泡茶 ==========");
        tea.prepareRecipe("茶");
    }
}

取舍对照:

维度传统模板方法(继承)函数式编排(组合)
复用机制继承,子类复用父类骨架组合,高阶函数注入步骤
流程骨架位置抽象类的 final 模板方法普通方法或独立流程函数
扩展方式新增子类传入不同的函数
是否强制流程固定是(final 锁死)取决于实现,可松可紧
钩子支持天然支持(虚方法)需额外参数或 Optional 模拟
类爆炸风险每个变体一个子类无新增类,只有函数
可读性类名即文档,流程一目了然短逻辑清晰,长逻辑易失控

经验小谈:如果流程骨架固定、可变步骤有 2~4 个、且需要被继承体系管理(比如框架扩展点),用传统模板方法;如果只是"固定顺序 + 几个可变动作"、不需要继承、逻辑简单,函数式编排更轻。但要注意:函数式编排已经偏离了模板方法模式的本意,更接近"策略 + 流程组合",不要为了套模式而硬改。


模板方法模式与策略模式的对比

模板方法模式和策略模式经常被放在一起比较,因为两者都涉及"算法的可变性"。但它们的实现机制、控制方向、适用场景完全不同。

一、主要区别

  • 模板方法模式:用继承复用流程骨架,父类控制流程,子类只填可变步骤。算法的结构不变,步骤实现可变。
  • 策略模式:用组合持有可替换的算法对象,客户端在运行时选择整个算法。算法的整体可替换。

用"冲泡饮品"类比:

  • 模板方法:冲泡流程(烧水 → 冲泡 → 倒杯 → 加调料)是固定的,你只能改"冲泡"和"加调料"两步;
  • 策略模式:整个"冲泡饮品"的算法就是一个策略,你可以换成"冲泡咖啡策略"“冲泡茶策略”“冲泡可可策略”,连流程都可以不同。

二、核心差异

对比维度模板方法模式策略模式
核心机制继承组合
可变范围流程中的某些步骤整个算法
流程骨架固定在父类模板方法中不在模式内约束,由策略自行决定
扩展方式新增子类,覆盖抽象/钩子方法新增策略类,实现策略接口
运行时切换不支持(实例化哪个子类就用哪套)支持(setStrategy() 替换引用)
耦合度子类与父类强耦合上下文只依赖策略接口,耦合较低
控制方向父类调用子类(好莱坞原则)上下文委托策略(组合)
开闭原则骨架不变,步骤可扩展算法整体可扩展
典型变化新增一种具体流程变体新增一种算法
客户端职责选择具体子类,调用模板方法选择策略、注入、可运行时切换

最关键的一条:

模板方法是"流程我定,你只填几步";策略模式是"整个算法你随便换,我只负责调用"。

三、用"冲泡饮品"分别实现,对比更直观

模板方法版本(继承,流程固定):

Beverage coffee = new Coffee();
coffee.prepareRecipe();   // 烧水 → 咖啡粉冲泡 → 倒杯 → 加糖和牛奶

Beverage tea = new Tea();
tea.prepareRecipe();      // 烧水 → 茶叶冲泡 → 倒杯(钩子跳过加调料)
  • 流程骨架在 Beverage.prepareRecipe() 里,final 锁死;
  • 子类只能改 brew()、addCondiments() 和钩子;
  • 不能在运行时把 coffee 变成 tea 的流程。

策略模式版本(组合,整体可换):

BeverageMaker maker = new BeverageMaker(new CoffeeStrategy());
maker.prepare();                         // 按咖啡策略冲泡
maker.setStrategy(new TeaStrategy());    // 运行时切换
maker.prepare();                         // 按茶策略冲泡
  • 整个冲泡算法被封装成策略,流程由策略自己决定;
  • 上下文只持有策略接口,可在运行时替换;
  • 策略之间互不知道彼此。

四、判断用哪个

问题选模板方法选策略
流程骨架是否固定?固定,只有少数步骤可变不固定,整个算法可替换
是否需要运行时切换?不需要需要
可变部分占整体比例?小(局部步骤)大(整体算法)
是否愿意用继承?可以,且希望复用骨架不希望继承,倾向组合
变化的核心是什么?同一流程的不同变体同一行为的不同算法

五、常见误区

  • 误区 1:模板方法就是"带 final 方法的策略模式"。 不是。模板方法靠继承复用骨架,策略靠组合替换整体;前者流程固定,后者算法整体可换。
  • 误区 2:模板方法不能有钩子。 钩子正是模板方法的扩展点,合理使用钩子能让子类按需介入流程,而不必重写整个骨架。
  • 误区 3:模板方法和策略模式不能共存。 可以。比如支付流程(模板方法固定"校验→扣款→记录→通知"),其中"扣款"这一步用策略模式支持微信/支付宝/银行卡。
  • 误区 4:模板方法一定比策略模式"低级"。 两者没有高下之分,只是适用场景不同。流程固定时,模板方法比策略模式更直接、更省类。

六、典型使用场景对照

模板方法模式策略模式
Servlet 的 service() → doGet()/doPost()支付方式:微信、支付宝、银行卡
Spring JdbcTemplate 的 execute()折扣计算:无折扣、VIP、SVIP
JUnit TestCase 的 setUp/test/tearDown排序算法:快排、归并、堆排
InputStream.read() 的骨架压缩算法:zip、rar、7z
数据导入:打开→读取→解析→转换→关闭出行方式:打车、地铁、自驾

一句话区分:模板方法是"流程骨架我定好,你只填可变步骤";策略模式是"整个算法你随便换,我只负责调用"。


模板方法模式的优缺点

优点

1. 复用流程骨架,消除重复代码。 固定步骤只在父类写一次,所有子类共享,避免每个子类重复实现相同流程。

2. 符合开闭原则,扩展性好。 新增一种流程变体,通常只需新增一个子类,实现抽象方法、按需覆盖钩子,父类骨架不用改。

3. 控制流程顺序,防止子类破坏结构。 模板方法声明为 final,子类无法改变步骤顺序,保证算法骨架的稳定性。

4. 钩子提供灵活扩展点。 子类可以在不重写整个流程的前提下,按需介入某些可选步骤。

5. 符合好莱坞原则。 “不要调用我,让我调用你”——父类控制流程,子类只提供实现,降低子类对流程的掌控负担。

6. 代码结构清晰,职责分明。 流程在父类一目了然,可变步骤在子类各司其职,阅读和维护成本低。

缺点

1. 继承导致强耦合。 子类与父类紧密绑定,父类骨架变更可能影响所有子类;子类也难以脱离父类单独复用。

2. 类数量会增加。 每种流程变体一个子类,变体一多,类的数量明显上升。

3. 不支持运行时切换。 实例化哪个子类,就固定走哪套步骤,无法在执行时动态替换流程实现(这正是策略模式的强项)。

4. 流程骨架变更成本高。 一旦骨架需要调整(增删步骤、改顺序),所有子类都可能受影响,甚至需要修改抽象方法签名。

5. 钩子滥用会削弱骨架约束。 钩子给得太多、太细,子类就能通过钩子绕过流程约束,模板方法"锁流程"的价值被稀释。

6. 受继承层次限制。 Java 单继承,子类若已继承其他类,就无法再继承模板方法抽象类;C++ 多继承虽可行,但会带来菱形继承等复杂性。

7. 测试需要覆盖父类骨架与子类步骤。 父类骨架要测流程顺序,子类要测可变步骤,测试成本比普通类略高。


一个反面例子

“不要为了模式而模式”,这里给一个具体的对照:

// 场景:只有 2 个步骤,且流程几乎不变
// 写法 A:直接写两个方法 —— 2 个方法,15 行
void makeCoffee() {
    boilWater();
    brewCoffee();
    pourInCup();
    addSugarAndMilk();
}

void makeTea() {
    boilWater();
    brewTea();
    pourInCup();
    addLemon();
}

// 写法 B:引入模板方法模式 —— 1 抽象类 + 2 子类 + 1 客户端
// 收益:新增饮品时不改骨架;
// 成本:文件数从 1 变 4,新人需先理解继承体系才能改一行逻辑。

判断标准:

  • 流程步骤 ≥ 3 且多个变体共享骨架 → 模板方法值得;
  • 流程只有 2~3 步、变体少、几乎不变 → 直接写方法更直观;
  • 流程固定但可变步骤多、且需要运行时切换 → 考虑模板方法 + 策略模式组合;
  • 流程不固定、整个算法可替换 → 策略模式更合适。

应用场景

以下场景是模板方法模式常用的地方,但未必只能用模板方法模式。判断时看两条:
① 是否存在固定的流程骨架;
② 是否有多个变体共享这个骨架,只是部分步骤不同。
如果整个算法都可替换,优先考虑策略模式;如果只需要创建对象,考虑工厂方法(它本身就是模板方法的一种特例)。

主流框架使用模板方法模式的场景

Java Servlet 的 HttpServlet。 service() 方法是模板方法,它根据请求方法把请求分派给 doGet()、doPost()、doPut()、doDelete() 等。service() 的流程骨架(解析请求 → 分派 → 处理)由 Servlet 容器控制,开发者只需继承 HttpServlet 并实现对应的 doXxx() 方法。

Spring 的 JdbcTemplate。 execute()、query()、update() 等方法都是模板方法:获取连接 → 创建 Statement → 执行 → 处理结果 → 释放资源,这一整套流程由 JdbcTemplate 固定,开发者只需提供 PreparedStatementSetter、RowMapper、ResultSetExtractor 等回调,专注 SQL 与结果映射。

Spring 的 AbstractApplicationContext.refresh()。 refresh() 是典型的模板方法,定义了容器启动的骨架:准备上下文 → 获取 BeanFactory → 加载 BeanDefinition → 后置处理 → 注册监听器 → 实例化单例 Bean → 完成刷新。其中若干步骤是抽象方法或钩子,由子类(如 ClassPathXmlApplicationContext、AnnotationConfigApplicationContext)实现。

Java InputStream / OutputStream。 InputStream.read(byte[], int, int) 是模板方法,骨架是"循环调用 read() 直到读满或结束",子类只需实现 read() 这个抽象方法。OutputStream.write(byte[], int, int) 同理。

JUnit 的 TestCase(JUnit 3)。 runBare() 是模板方法,骨架是 setUp() → testXxx() → tearDown(),开发者只需实现测试方法和可选的 setUp/tearDown。JUnit 4/5 用注解替代了继承,但思路一脉相承。

Android Activity 生命周期。 onCreate()、onStart()、onResume()、onPause()、onStop()、onDestroy() 的调用顺序由框架控制,开发者按需覆盖,本质上是模板方法思想。

其他场景

数据导入与导出。 "打开文件 → 读取内容 → 解析 → 转换为领域对象 → 关闭资源"是固定骨架,解析步骤因 Excel、CSV、JSON、XML 而异。用模板方法固定骨架,子类只实现解析。

支付流程。 "参数校验 → 风控检查 → 扣款 → 记录流水 → 发送通知"是常见骨架,扣款步骤因渠道而异。可以用模板方法固定流程,用策略模式处理扣款渠道,两者组合。

文档生成。 "收集数据 → 渲染模板 → 填充内容 → 导出文件"是固定流程,渲染引擎(Freemarker、Thymeleaf、Word 模板)因格式而异。

游戏主循环。 "初始化 → 循环(输入 → 更新 → 渲染)→ 清理"是经典骨架,具体更新与渲染逻辑由子类或回调实现。

构建工具的生命周期。 Maven 的 clean、compile、test、package、install 是固定阶段,具体插件实现各阶段行为。

注意:如果"整个算法"都可替换、且需要运行时切换,这更接近策略模式;如果只是"创建对象"这一步可变,工厂方法就是模板方法的特例,不必再套一层模板方法。

审批流程。 "提交 → 初审 → 复审 → 终审 → 归档"是固定骨架,每个审批节点的具体规则因业务而异。用模板方法固定节点顺序,子类实现各节点规则。

测试执行框架。 "准备环境 → 执行测试 → 断言 → 清理环境"是固定骨架,具体测试逻辑由子类或回调提供。


注意事项

必读:模板方法与骨架设计

1. 模板方法应声明为 final。 这是模板方法模式的关键约束。如果允许子类覆盖模板方法,流程骨架就可能被篡改,"固定流程"的价值不复存在。C++ 用 final 关键字,Java 用 final 修饰符。

2. 抽象方法只放真正必须由子类实现的步骤。 不要把固定步骤也声明为抽象方法,否则子类被迫重复实现相同逻辑,违背复用初衷。

3. 钩子方法要克制。 钩子是扩展点,不是"万能开关"。给太多钩子,子类就能绕过流程约束;给太少,扩展性不足。钩子的默认实现要合理,且要写清楚"覆盖后会发生什么"。

4. 流程骨架要稳定。 模板方法一旦被大量子类依赖,增删步骤、调整顺序的成本很高。设计时要考虑清楚:哪些步骤是真正固定的,哪些是可能变化的,是否要给未来留扩展点。

5. 抽象类的职责要单一。 一个抽象类只应定义一个流程骨架。把多个不相关的流程塞进同一个抽象类,会导致子类被迫实现不需要的抽象方法。

必读:继承与耦合

6. 子类与父类是强耦合关系。 父类骨架变更会影响所有子类,父类的 protected 成员也会暴露给子类。设计时要控制 protected 成员的可见范围,避免子类过度依赖父类内部细节。

7. 注意里氏替换原则。 子类覆盖钩子或具体方法时,不应改变父类约定的语义。比如钩子 customerWantsCondiments() 默认返回 true,子类覆盖后不应产生与"是否加调料"无关的副作用。

8. Java 单继承的限制要提前考虑。 如果具体类已经继承了其他基类,就无法再继承模板方法抽象类。此时可以考虑用组合 + 函数式回调替代继承(见「函数式轻量变体」),或者用接口默认方法模拟。

9. C++ 多继承要谨慎。 模板方法抽象类作为基类时,注意虚继承、菱形继承和析构函数的虚化,避免资源泄漏和歧义。

必读:钩子与扩展点

10. 钩子方法的命名要能表达"是否/要不要"的语义。 比如 customerWantsCondiments()、shouldSkipValidation()、isLoggingEnabled(),一看就知道是可选开关。

11. 钩子的默认行为要安全且合理。 默认实现应该是最常见、最保守的行为,子类覆盖时应能明确感知到行为变化。不要让钩子的默认值成为"隐藏地雷"。

12. 钩子不要替代抽象方法。 如果某个步骤是流程必需的、且没有合理默认值,应该声明为抽象方法强制子类实现,而不是用钩子"可选覆盖"。

进阶:工程落地

13. 模板方法与策略模式可以组合使用。 流程骨架固定用模板方法,其中某个可变步骤用策略模式支持运行时切换。比如支付流程用模板方法固定"校验→扣款→记录→通知","扣款"用策略模式支持多渠道。

14. 模板方法要与工厂方法区分。 工厂方法本身就是模板方法的一种特例(创建对象这一步可变)。如果只是"创建对象"可变,用工厂方法即可,不必再套一层模板方法。

15. 框架扩展点优先用钩子,而不是抽象方法。 框架给使用者的扩展点,尽量设计成钩子(有默认实现),降低使用者的实现负担;只有真正必须实现的步骤才用抽象方法。

16. 注意模板方法的调用栈深度。 模板方法 → 抽象方法 → 子类实现,调用栈比普通方法深一层。在性能敏感场景(如高频调用的解析器)要评估开销。

17. 序列化场景要小心。 如果抽象类或子类需要序列化,注意 final 模板方法、protected 成员、钩子状态的序列化兼容性。跨版本反序列化时,骨架变更可能导致兼容问题。

进阶:可维护性与测试

18. 不要为了模式而模式。 如果流程只有两三步、变体少、几乎不变,直接写方法更直观(见「一个反面例子」)。

19. 警惕子类爆炸。 每种流程变体一个子类,变体一多类就多。可以通过合并真正相近的变体、用钩子参数化差异、用组合替代继承来缓解。

20. 抽象类命名要表达"流程"而非"实现"。 比如 Beverage、DataImporter、PaymentFlow,而不是 AbstractBeverageImpl、BaseHandler。

21. 文档要写清楚每个抽象方法和钩子的契约。 子类实现时需要知道:方法何时被调用、参数含义、返回值语义、是否允许抛异常、钩子覆盖后的影响。

22. 模板方法要单独测试流程顺序。 用一个测试子类记录各步骤调用顺序,断言模板方法确实按约定顺序执行。子类的抽象方法实现也要独立测试。

23. 钩子的分支要覆盖测试。 钩子返回 true / false 两条路径都要测,避免默认分支之外的路径长期无人覆盖。

24. 异常处理策略要统一。 抽象方法抛出的异常类型应在抽象类层约定,模板方法可以统一捕获、包装或转换,避免子类各自抛不同异常导致调用方难以处理。


模板方法模式不负责什么

把职责误加给抽象类或模板方法,是模板方法模式被用坏的最常见原因。明确边界,比记住定义更重要:

职责谁负责不该由谁负责
选择哪个具体子类客户端、工厂、DI 容器抽象类(抽象类不知道子类)
运行时切换流程实现不支持;需要切换请用策略模式模板方法(实例化即固定)
动态改变流程骨架修改抽象类,重新编译子类(模板方法应 final)
步骤之间的数据传递方法参数、返回值、受保护的成员字段模板方法(只负责按顺序调用)
子类步骤的异常兜底子类自身或模板方法统一包装抽象方法(应保持契约清晰)
钩子的业务语义子类覆盖时自行保证抽象类(只提供默认行为)
创建具体子类实例客户端、工厂、DI 容器抽象类(除非它同时是工厂)

一句话:模板方法模式只做一件事——把"流程骨架"从"步骤实现"里分离出来,让骨架复用、步骤可变。其余的事情,交给客户端、工厂、策略模式或业务层。


总结

  • 流程骨架固定,可变步骤下放子类;模板方法锁顺序,抽象方法填步骤,钩子留扩展。
  • 接口定契约、抽象类定骨架、子类填步骤、客户端一次调用。
  • 是否值得用,看两条:流程是否固定且多变体共享;可变部分是否只占少数步骤。二者缺一,先写普通方法。
  • 落地时记住三件事:模板方法 final、钩子要克制、运行时切换请找策略模式。
EfficientNet中的MBConv模块详解:为什么它能如此高效? 本文深度解析了EfficientNet中的MBConv模块,详细介绍了其核心设计理念、效率优化技术和复合缩放理论。MBConv通过深度可分离卷积、倒残差结构和注意力机制三大关键技术,实现了计算效率与模型性能的平衡,成为高效网络结构设计的典范。 阅读详情

相关推荐

SRAM Bank数量配置:性能、面积与功耗的权衡指南

SRAM(静态随机存取存储器)是芯片设计中的关键存储单元,其Bank数量配置直接影响访问延迟、吞吐率、功耗和面积。Bank切分通过将存储阵列分区,缩短位线和字线延迟,从而提升性能,但会增加外围电路开销。合理配置Bank数量可优化PPA(性能、功耗、面积),在28nm和16nm工艺下,2个Bank通常是性价比最高的选择。本文基于实测数据,探讨不同场景下的Bank配置策略,帮助工程师避免常见误区,提升设计效率。

weixin_27230891的博客 179

设计模式-模板方法模式-Template Method Pattern

首先,我们定义一个模板基类,其中包含模板方法和一些虚函数,这些虚函数将在派生类中被重写。// 模板基类public:// 模板方法step1();step2();step3();protected:// 抽象步骤,子类必须实现创建一个抽象基类,它定义了模板方法和算法的框架。模板方法:一个或多个步骤组成的算法,这些步骤通过调用抽象操作来执行。抽象操作:定义为纯虚函数(pure virtual functions),子类必须实现这些函数。

wesigj的博客 1293

UG476 GT Transceivers文档翻译和自身理解

对UG476 Xilinx官方文档进行翻译的基础上加上自己的理解

weixin_45143788的博客 4600

【设计模式】——模板方法模式(Template Method Pattern)

模板方法模式通过定义一个算法的骨架,允许子类在不改变算法结构的前提下,通过重写某些步骤的具体实现来定制算法的行为。这种模式提高了代码的复用性和可维护性,使得算法的结构更加清晰,同时也便于扩展和维护。在C++中,模板方法模式是一种非常实用的设计模式,通过它我们可以构建出灵活且易于扩展的软件系统。

J^T的博客 1037

同步辐射材料科学应用指南:从XAFS原位实验到数据处理

材料结构分析是材料研究的核心环节,但传统实验室X射线受限于能量固定、亮度不足,往往难以回答原子局域配位、非晶结构及动态演变等关键问题。同步辐射光源以高通量、连续可调、偏振可控及脉冲时间结构等特性,将材料表征从静态观察推进到原位动态追踪层次。基于X射线吸收精细结构谱(XAFS)、原位实验、X射线衍射与散射、微区成像等技术,研究人员可实现元素选择性分析、近邻原子环境解析及真实工况下的结构演变监测。本文从技术原理出发,覆盖实验方案设计、样品制备、现场对光、数据处理与常见故障排查,并结合典型应用场景,为材料科学领域

weixin_32612263的博客 173

Qt 侧边菜单栏+堆叠窗口stackedWidget

效果如下:环境:Qt 6.6.2 MSVC2019 64bit。

weixin_41033231的博客 1568

设计模式教程:模板方法模式(Template Method Pattern)

定义一个操作中的算法骨架,将一些步骤延迟到子类中实现。通过模板方法,子类可以重新定义算法中的某些步骤,而无需改变算法的结构。

遇见伯灵说的博客 1381

Cadence画PCB的傻瓜式教程

1.原理图1)建立元件库2)建立原理图3)在原理图中画好电路图4)利用MENTOR-GRAPHICS-IPC-7351-LP-VIEWER查看元件的封装信息,用pad designer画焊盘,用pcb editor画元件的封装(对照LP_VIEWER),(也可以使用package wizard)5)在原理图中为元件添加封装信息(键入)6)Drc检查7)生成网表2.Pcb1)设置...

tianpu2320959696的博客 1万+

23种设计模式——模板方法模式(Template Method Pattern)详解

本文介绍23种经典设计模式之——模板方法模式(Template Method Pattern)的介绍及使用

Meteors.的博客 826

c#【保姆级教程】基于System.Text.Json 支持多态类型序列化! 手把手教你实现一个通用的 PolymorphicJsonConverter!

我们需要自己实现一个继承自的转换器,在序列化时记录具体类型,在反序列化时根据类型标识还原成正确的子类对象。public abstract class WorkStepBase { /* 抽象方法等 */ }public class Person : WorkStepBase { /* 实现 */ }public class Product : WorkStepBase { /* 实现 */ }特性说明✅ 自动发现子类无需手动维护类型列表,自动扫描程序集✅ 类名映射。

一个编程的同学 425

【100%通过率】华为OD机试真题 C 实现【最多等和不相交连续子序列】【2022.11 Q4 新题】

给定一个数组,我们称其中连续的元素为连续子序列,称这些元素的和为连续子序列的和。数组中可能存在几组连续子序列,组内的连续子序列互不相交且有相同的和。求一组连续子序列,组内子序列的数目最多。输出这个数目。

MISAYAONE的博客 1万+

设计模式(十四)——模板方法模式(Template Method Pattern)

模板方法模式(Template Method Pattern) 基本介绍 模板方法模式是编程中经常用得到模式。它定义了一个操作中的算法骨架,将某些步骤延迟到子类中实现。这样,新的子类可以在不改变一个算法结构的前提下重新定义该算法的某些特定步骤。 核心 处理某个流程的代码已经都具备,但是其中某个节点的代码暂时不能确定。因此,我们采用模板方法模式,将这个节点的代码实现转移给子类完成。即:处理步骤父类中...

Duktig的博客 360

【定位模块项目实战】gnss_localizer、lidar_localizer功能包解析

系列文章目录 提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 TODO:写完再整理 文章目录系列文章目录前言一、1.2.3.4.二、1.2.3.4.三、1.2.3.4.四、1.2.3.4.总结参考资料 前言 认知有限,望大家多多包涵,有什么问题也希望能够与大家多交流,共同成长! 本文先对**【无人驾驶autoware项目实战】感知–定位模块**做个简单的介绍,具体内容后续再更,其他模块可以参考去我其他文章 提示:以下是本篇文章正文内容 一、 1. 2. 3. 4. . . 二、

qq_35635374的博客 1931

Java设计模式之模板方法模式(Template Method Pattern)

假设需要实现制作茶和咖啡的流程,两者步骤类似(烧水、冲泡、倒入杯子),但具体冲泡方法和添加调料不同。:框架定义通用流程,允许用户自定义部分步骤(如Servlet生命周期、JUnit测试流程)。:当多个类有相似的流程,但某些步骤实现不同时(如数据处理流程、文件解析流程)。:适用于具有固定流程但部分步骤需要定制的场景,如框架扩展、多态流程处理。,用于定义算法的骨架,将某些步骤的具体实现延迟到子类中。:通过父类定义算法流程,子类灵活实现具体步骤,实现代码复用和流程控制。方法为模板方法,由框架调用。

guanxin0206的博客 673

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

JAVA设计模式之 模板方法模式【Template Method Pattern】

一、概述 二、适用chuangk

小吕丶Code阶区 3481

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

设计模式——模板方法模式( Template Method Pattern)

一、模板方法模式的定义 在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。 从定义可以看出,模板方法的本质是:封装算法。类图结构: 其中,templateMethod会调用抽象类中定义的一些抽象方法,或者已经实现(默认的具体实现)的方法(称之为“钩子”),子类(实现类)需要实现父类中的抽象方法,或者覆盖父类中已实现

蜗牛的专栏 804

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

设计模式(行为型)之模板方法模式(Template Method Pattern)

模板方法模式是一种基于继承的代码复用,它是一种类行为型模式;是结构最简单的行为型设计模式,在其结构中只存在父类与子类之间的继承关系。通过使用模板方法模式,可以将一些复杂流程的实现步骤封装在一系列基本方法中,在抽象父类中提供一个称之为模板方法的方法来定义这些基本方法的执行次序,而通过其子类来覆盖某些步骤,从而使得相同的算法框架可以有不同的执行结果。模板方法模式提供了一个模板方法来定义算法框架,而某些具体步骤的实现可以在

工匠若水 5070

【设计模式】之模板方法模式

今天给大家介绍23种设计模式中的模板方法模式,这个设计模式在Spring框架中还是挺常见的,手写Spring源码的小伙伴必须要掌握的设计模式之一,文章结构还是经典的三连问,是什么?为什么?怎么用?🌈

小杰不秃头的博客 1568

【设计模式|行为型】模板方法模式(Template Method Pattern)

详细介绍模板方法模式(Template Method Pattern),并说明优缺点与使用场景模板方法模式是一种行为设计模式,它定义了一个算法的骨架,将一些步骤的实现延迟到子类中。在模板方法模式中,一个抽象类公开定义了执行它的方法的方式/顺序,并将一些步骤的具体实现委托给子类。模板方法模式是一种典型的通过继承来实现代码复用的设计模式,它遵循了开闭原则,即抽象类定义了算法的骨架,而具体步骤的实现可以在子类中灵活变化。

zxcyxg123的博客 266

设计模式之模板方法模式(Template Method Pattern)

其实模板方法在我们平时工作使用的非常频繁,但是没有学习过设计模式的小伙伴可能不是很清楚模板方法的定义是什么,即使是自己一直在用。 模板方法模式定义 Define the skeleton of an operation,deferring some steps to subclasses. Template Method lets subclasses redefine certain step...

程铭程铭你快成名的博客 3520

如何让孩子爱上设计模式 ——24.模板方法模式(Template Method Pattern)

如何让孩子爱上设计模式 ——24.模板方法模式(Template Method Pattern)标签: 设计模式初涉描述性文字终于来到23种设计模式的最后一种:模板方法模式,听名字感觉好像很复杂的样子, 实则不然,先不讲概念,先写个简单的例子,通过例子来感受这个模式。代码示例既然是最后一节的了,还是要走一波回忆杀的,还记得之前小猪奶茶店的例子吗? 在工厂方法模式那一章,我们写了奶茶的制作过程过程

Coder-Pig的猪栏 17万+

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

本文由@呆代待殆原创,转载请注明出处:http://www.cnblogs.com/coffeeSS/ 模板方法模式简介 这是一个被用的相当广泛的一种设计模式,变体也特别多,他建立一个抽象类定义一个算法的结构,但是把细节部分的实现权力交个子类实现,是一种很灵活且代码重用性很高的设计模式。 模板方法模式的定义和基本结构 定义:在一个方法中定义一个算法的骨架,而将其中一些步骤的实现...

weixin_30551947的博客 149

二、16【设计模式】之模板方法模式

今天的博客主题 设计模式——》设计模式之模板方法模式 模板方法模式 TMP(Template Method Pattern) 定义 定义一个操作的算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤。 模板方法实际上是封装了一个固定流程,该流程由几个步骤组成,每个步骤有不同的子类实现,从而让固定的流程产生不同得结果。很简单,就是类的继承机制,也是应用很广泛的一个模式。 其本质就是抽象封装流程,进行具体实现。 ...

刘总Java 178

设计模式 —— 模板方法模式(Template method Pattern)

模板方法模式(Template method Pattern)概念: 模板方法模式 在一个方法中定义一个算法的 骨架,而将一些步骤延迟到子类。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。 组成:templateMethod(模板方法):定义了某个算法的模板顺序或一组步骤,其中任何步骤都可以是抽象的,由子类来实现。primitiveOperation1(抽象步骤方法):

IT小小鸟~~ 781

JavaScript设计模式之模板方法模式

在JavaScript开发中用到继承的场景其实并不是很多,很多时候我们都喜欢用mix-in的方式给对象扩展属性。但这不代表继承在JavaScript里没有用武之地,虽然没有真正的类和继承机制,但我们可以通过原型prototype来变相地实现继承。 不过这里并非要讨论继承,而是讨论一种基于继承的设计模式——模板方法(Template Method)模式。 模板方法模式的定义和组成 模板方法模式是一种...

wuyufa1994的博客 1405
上一篇: 设计模式之策略模式
下一篇: 设计模式之访问者模式
LiaoYuan.Dai
博客等级 码龄7年 247粉丝 24原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值