模板方法模式:框架中的算法骨架

FreeGuideOnline 最新 2026-06-18

模板方法模式:框架中的算法骨架

你是否曾想过,为什么很多框架都要求你按照特定的步骤去重写某些方法?它们如何在“不变”中容纳“万变”?这就是模板方法模式的精髓。

什么是模板方法模式?

模板方法模式是一种行为设计模式,它在父类中定义了一个算法的骨架,将某些步骤延迟到子类中实现。这样可以在不改变算法整体结构的情况下,让子类重新定义算法中的某些特定步骤。

打个比方:泡一杯咖啡和泡一杯茶的流程惊人地相似——都需要烧水、冲泡、倒入杯中、添加配料。区别在于“冲泡”的具体内容(咖啡粉 vs 茶叶)和“添加配料”(糖奶 vs 柠檬)。模板方法模式就把这些相似流程抽象成一个骨架,而把具体变化的部分交给子类去完成。

核心思想

  • 不变的部分:算法的“骨架”,由父类中的模板方法定义。
  • 可变的部分:某些特定步骤的实现,由子类提供。

模式结构

模板方法模式包含以下角色:

  • 抽象类(AbstractClass):定义模板方法,并实现算法中不变的公共步骤。同时声明抽象方法,供子类实现差异化逻辑。
  • 具体类(ConcreteClass):实现抽象类声明的抽象方法,完善算法的具体行为。
+---------------------+
|   AbstractClass     |
|---------------------|
| + templateMethod()  |<-- 模板方法(final,防止子类修改算法骨架)
| + step1()           |
| + step2()           |<-- 某些步骤可以是抽象方法
| + hook()            |<-- 钩子方法(可选,提供默认行为)
+---------------------+
          |
+---------------------+
|   ConcreteClass     |
|---------------------|
| + step2()           |<-- 实现抽象方法
| + hook()            |<-- 可选重写钩子
+---------------------+
  • 模板方法 通常声明为 final,防止子类修改算法顺序。
  • 抽象方法 强制子类提供实现。
  • 钩子方法(Hook) 提供默认(通常是空)行为,子类可以选择覆盖以干预算法走向。

动手实现一个示例

我们用 Java 演示一个“数据挖掘”的例子:从不同格式文件(CSV、PDF)中提取数据并生成报告。流程固定为:打开文件 -> 提取数据 -> 分析数据 -> 生成报告 -> 关闭文件。

步骤 1:创建抽象类

public abstract class DataMiner {
    // 模板方法,定义算法骨架
    public final void mine(String path) {
        openFile(path);
        String rawData = extractData();
        Data analyzed = analyzeData(rawData);
        generateReport(analyzed);
        closeFile(path);
    }

    // 默认实现的步骤(不变部分)
    private void openFile(String path) {
        System.out.println("打开文件: " + path);
    }

    private void closeFile(String path) {
        System.out.println("关闭文件: " + path);
    }

    // 抽象方法,子类必须提供实现
    protected abstract String extractData();
    protected abstract Data analyzeData(String rawData);

    // 钩子方法,子类可选重写
    protected void generateReport(Data data) {
        System.out.println("生成默认报告: " + data);
    }
}

步骤 2:实现具体类

// CSV 数据挖掘器
public class CsvDataMiner extends DataMiner {
    @Override
    protected String extractData() {
        System.out.println("从CSV中提取数据...");
        return "csv,data,content";
    }

    @Override
    protected Data analyzeData(String rawData) {
        System.out.println("分析CSV数据...");
        return new Data("CSV分析结果");
    }
    // 使用默认的 generateReport
}

// PDF 数据挖掘器
public class PdfDataMiner extends DataMiner {
    @Override
    protected String extractData() {
        System.out.println("从PDF中提取数据...");
        return "pdf text extracted";
    }

    @Override
    protected Data analyzeData(String rawData) {
        System.out.println("分析PDF数据...");
        return new Data("PDF分析结果");
    }

    @Override
    protected void generateReport(Data data) {
        System.out.println("生成PDF专属报告: " + data);
    }
}

步骤 3:客户端调用

public class Client {
    public static void main(String[] args) {
        DataMiner csvMiner = new CsvDataMiner();
        csvMiner.mine("data.csv");

        System.out.println("---");

        DataMiner pdfMiner = new PdfDataMiner();
        pdfMiner.mine("report.pdf");
    }
}

输出:

打开文件: data.csv
从CSV中提取数据...
分析CSV数据...
生成默认报告: CSV分析结果
关闭文件: data.csv
---
打开文件: report.pdf
从PDF中提取数据...
分析PDF数据...
生成PDF专属报告: PDF分析结果
关闭文件: report.pdf

钩子方法的妙用

钩子方法让模板方法更加灵活。例如,我们可以在抽象类中添加一个 boolean isReportRequired() 钩子,允许子类决定是否生成报告。

public abstract class DataMiner {
    // ... 其他方法

    // 钩子:是否需要生成报告
    protected boolean isReportRequired() {
        return true;   // 默认需要
    }

    public final void mine(String path) {
        openFile(path);
        String rawData = extractData();
        Data analyzed = analyzeData(rawData);
        if (isReportRequired()) {          // 钩子控制
            generateReport(analyzed);
        }
        closeFile(path);
    }
}

子类通过覆盖钩子来改变行为:

public class CsvDataMiner extends DataMiner {
    // ...
    @Override
    protected boolean isReportRequired() {
        return false;  // CSV挖掘不生成报告
    }
}

这样,CsvDataMiner 在执行中将跳过报告生成步骤。

模板方法模式的优缺点

优点

  • 复用代码:将公共行为提取到父类,避免重复代码。
  • 控制扩展:只允许子类改变特定步骤,算法整体结构受保护。
  • 符合开闭原则:通过增加子类扩展功能,无需修改现有代码。
  • 好莱坞原则:“别调用我们,我们会调用你”。父类控制流程,子类只提供实现。

缺点

  • 类数量增加:每新增一种算法变体都要新建子类。
  • 灵活性受限:算法骨架固定,如果需要调整步骤顺序就不得不修改抽象类(违反开闭原则)。
  • 设计复杂度:父类中的抽象方法和钩子方法需要提前规划。

实际应用场景

模板方法模式随处可见,特别是在框架开发中:

  • Java Servletservice() 方法作为模板,doGet(), doPost() 是留给子类的具体步骤。
  • Spring 框架:各种 Template 类(如 JdbcTemplateRestTemplate)封装了资源获取、异常处理等不便步骤,用户只需提供一小段回调。
  • 单元测试框架:JUnit 的 setUp(), tearDown() 就是在 runBare() 模板中的钩子。
  • 游戏开发:初始化、游戏循环、渲染、更新状态等固定流水线。

与其他模式的关系

  • 工厂方法模式 常被模板方法模式调用,用于创建算法中需要的产品对象。
  • 策略模式 是通过组合改变整个算法,而模板方法通过继承改变部分步骤。策略模式更动态,模板方法更静态但结构清晰。
  • 建造者模式 也关注一步步构建,但建造者偏向于对象的装配过程,模板方法偏向于算法流程控制。

总结

模板方法模式通过将算法骨架固定在父类中,并让子类填充细节,实现了代码复用控制反转。它非常适合那些算法结构稳定、个别步骤存在变化的场景。当代框架的“填空式”开发体验,很多都源自这个简单而强大的模式。

下次当你继承一个框架类,并重写其中某个 doXXX() 方法时,你就知道正身处模板方法模式的舞台之上。

延伸练习

  1. 为示例中的数据挖掘器增加一个“数据清洗”步骤,并在 PDF 挖掘器中跳过该步骤。
  2. 为什么模板方法常标记为 final?如果去掉 final 会有什么风险?
  3. 在你自己项目中找出一段多个方法流程相似的代码,尝试用模板方法模式重构。