模板方法模式完全指南:从 Spring JdbcTemplate 到 Servlet 与框架设计

模板方法模式是 GoF 23 个模式里"最像继承本身"的一个 —— 它的核心思想就是"把算法的骨架定义在父类,具体步骤的实现交给子类"。Spring 的 JdbcTemplate / RestTemplate、Servlet 的 service 方法、Junit 的测试生命周期、几乎所有用 "Template" 命名的类背后都是它。这篇文章把模板方法讲透,讲清楚它和策略模式、装饰器、钩子的关系。

问题:多个类有相似流程但部分步骤不同

看一个常见场景:从不同数据源导入数据,流程都是"打开 → 读 → 转换 → 入库 → 关闭",但每一步具体实现不同:

class CsvImporter {
    public void importData() {
        // 1. 打开
        BufferedReader r = new BufferedReader(new FileReader("data.csv"));
        // 2. 读
        List<String> lines = r.lines().collect(toList());
        // 3. 转换
        List<Record> records = lines.stream()
            .map(l -> new Record(l.split(",")))
            .collect(toList());
        // 4. 入库
        for (Record rec : records) db.save(rec);
        // 5. 关闭
        r.close();
    }
}

class JsonImporter {
    public void importData() {
        InputStream s = new FileInputStream("data.json");
        List<Map> jsons = new ObjectMapper().readValue(s, List.class);
        List<Record> records = jsons.stream().map(Record::fromMap).collect(toList());
        for (Record rec : records) db.save(rec);
        s.close();
    }
}

class XmlImporter { /* 类似但 XML 解析 */ }

问题:三个类有相同的"流程骨架",但每一步实现不同。代码大量重复 —— "入库""关闭"完全一样,"读"和"转换"只是格式不同。如果将来加"导入完成发邮件"这一步,要改三个类。

模板方法说:把骨架抽到父类,可变步骤声明为抽象方法,让子类填充

模板方法的标准结构

// 抽象类:定义算法骨架(模板方法)
abstract class DataImporter {
    // 模板方法:声明 final,防止子类覆盖整个流程
    public final void importData() {
        Object data = open();
        List<Record> records = parse(data);
        for (Record r : records) save(r);
        close(data);
        afterImport();    // 钩子,默认空
    }

    protected abstract Object open();
    protected abstract List<Record> parse(Object data);
    protected abstract void close(Object data);

    // 通用步骤,父类实现
    protected void save(Record r) {
        db.save(r);
    }

    // 钩子方法:默认空实现,子类可选覆盖
    protected void afterImport() {}
}

// 具体类:CSV 实现
class CsvImporter extends DataImporter {
    private final String path;
    public CsvImporter(String path) { this.path = path; }

    protected Object open() {
        try { return new BufferedReader(new FileReader(path)); }
        catch (IOException e) { throw new RuntimeException(e); }
    }
    protected List<Record> parse(Object data) {
        BufferedReader r = (BufferedReader) data;
        return r.lines().map(l -> new Record(l.split(","))).collect(toList());
    }
    protected void close(Object data) {
        try { ((BufferedReader) data).close(); } catch (Exception e) {}
    }
}

class JsonImporter extends DataImporter {
    private final String path;
    public JsonImporter(String path) { this.path = path; }

    protected Object open() {
        try { return new FileInputStream(path); }
        catch (Exception e) { throw new RuntimeException(e); }
    }
    protected List<Record> parse(Object data) {
        try {
            List<Map> raw = new ObjectMapper().readValue((InputStream) data, List.class);
            return raw.stream().map(Record::fromMap).collect(toList());
        } catch (Exception e) { throw new RuntimeException(e); }
    }
    protected void close(Object data) {
        try { ((InputStream) data).close(); } catch (Exception e) {}
    }

    // 覆盖钩子:JSON 导入完发邮件
    protected void afterImport() {
        emailService.send("admin@x.com", "JSON 导入完成");
    }
}

// 使用
new CsvImporter("data.csv").importData();
new JsonImporter("data.json").importData();

对比改造前后:

  • 骨架在父类,加新步骤(如"导入前先备份")只改父类一处。
  • 每个子类只关心自己"特殊"的部分,样板代码全部消失。
  • 钩子方法让子类选择性扩展 —— 不覆盖就什么也不发生。

钩子方法 vs 抽象方法

这是模板方法的核心区分:

  • 抽象方法:父类无法实现,子类必须实现。例:open / parse / close。
  • 钩子方法:父类提供默认(通常是空),子类选择性覆盖。例:afterImport。

钩子让模板方法极其灵活 —— 子类不必关心所有扩展点,只覆盖自己关心的。这种"默认不动,需要时再插"的设计在框架里特别常见。

实战 1:Spring JdbcTemplate

// Spring JdbcTemplate.query 内部就是模板方法
public <T> List<T> query(String sql, RowMapper<T> rowMapper) {
    // 模板:连接 -> 准备语句 -> 执行 -> 映射 -> 关闭
    Connection conn = getConnection();
    try (PreparedStatement ps = conn.prepareStatement(sql);
         ResultSet rs = ps.executeQuery()) {
        List<T> result = new ArrayList<>();
        while (rs.next()) {
            result.add(rowMapper.mapRow(rs, rs.getRow()));   // 让客户端决定每行变成什么
        }
        return result;
    } finally {
        releaseConnection(conn);
    }
}

// 使用:你只需要写 RowMapper(可变部分)
List<User> users = jdbcTemplate.query(
    "SELECT id, name FROM users",
    (rs, i) -> new User(rs.getLong("id"), rs.getString("name"))
);

这里有个小变体:Spring 用 RowMapper 作为参数(Strategy 风格)而不是子类化(经典模板方法风格)。两种实现都对,后者用组合替代继承,在 Java 8+ 用 Lambda 更轻量。

实战 2:Servlet

public abstract class HttpServlet {
    // 模板方法
    protected void service(HttpServletRequest req, HttpServletResponse resp) {
        String method = req.getMethod();
        switch (method) {
            case "GET":  doGet(req, resp); break;
            case "POST": doPost(req, resp); break;
            case "PUT":  doPut(req, resp); break;
            case "DELETE": doDelete(req, resp); break;
            // ...
        }
    }

    // 钩子(默认返回 405 Method Not Allowed,子类按需覆盖)
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        resp.sendError(405);
    }
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
        resp.sendError(405);
    }
    // ...
}

// 用户继承
public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        resp.getWriter().write("Hello");
    }
}

所有 Servlet 子类都遵循"GET → doGet,POST → doPost"这个模板。你只写自己关心的方法,其他自动 405。这是 Java Servlet 几十年保持稳定的设计基础。

实战 3:JUnit 测试生命周期

public abstract class TestCase {
    public void runBare() throws Throwable {
        setUp();
        try {
            runTest();
        } finally {
            tearDown();
        }
    }

    protected void setUp() {}        // 钩子
    protected void tearDown() {}      // 钩子
    protected abstract void runTest();
}

public class MyTest extends TestCase {
    private Database db;
    protected void setUp() { db = new Database(); db.connect(); }
    protected void tearDown() { db.close(); }
    protected void runTest() { assert(db.query("...") != null); }
}

JUnit 早期版本用继承 + 模板方法。现在用注解(@BeforeEach / @AfterEach)更灵活,但底层执行流程依然是模板方法。

实战 4:Spring Boot 启动流程

SpringApplication.run() 就是一个巨型模板方法:

// 简化版
public ConfigurableApplicationContext run(String... args) {
    // 1. 启动监听器
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting();

    // 2. 准备环境
    ConfigurableEnvironment environment = prepareEnvironment(listeners, args);

    // 3. 创建 ApplicationContext
    ConfigurableApplicationContext context = createApplicationContext();

    // 4. 准备 context
    prepareContext(context, environment, listeners, args);

    // 5. 刷新 context(加载 Bean)
    refreshContext(context);

    // 6. 调用 Runners
    callRunners(context, args);

    // 7. 启动完成
    listeners.started(context);
    return context;
}

每一步都是钩子,可以通过实现 ApplicationContextInitializerApplicationRunnerCommandLineRunner@PostConstruct 等扩展。Spring Boot 的"自动配置 + 可扩展"哲学就建立在这种模板方法 + 钩子点的设计上。

模板方法 vs 策略

两者都解决"算法可变"的问题,但实现机制不同:

  • 策略:组合。Context 持有 Strategy 对象,运行时可换。
  • 模板方法:继承。子类继承父类,编译期就定了。

选择标准:

  • 整个算法都可换 → 策略。
  • 算法骨架固定,只有几步可变 → 模板方法。
  • 需要运行时切换 → 策略。
  • 父类有大量公共代码可复用 → 模板方法。

现代实践更倾向于策略(组合优于继承),但模板方法在"框架定义流程 + 用户填空"的场景里仍然不可替代。

反转控制(IoC)的核心思想就在这

模板方法体现了"好莱坞原则"(Hollywood Principle):"Don't call us, we'll call you"。客户端不主动调用框架,而是实现框架定义的钩子,框架在合适时机回调。这种"控制权倒置"是所有框架区别于普通库的根本特征。

Spring、Hibernate、JUnit、Servlet、Android SDK 之所以叫"框架",就是因为它们用模板方法定义了流程,让你在流程的各个钩子点填代码。

钩子点的设计原则

  1. 钩子要够多,让用户能在关键节点扩展(before/after/around)。
  2. 钩子要够少,避免接口爆炸 —— 5-10 个钩子是合理范围。
  3. 钩子有合理默认,不强制子类全部实现。
  4. 钩子的顺序和契约要明确文档化:setUp 在 runTest 前,tearDown 在 runTest 后(即使 runTest 抛异常)。

各语言里的模板方法

Python:abstractmethod

from abc import ABC, abstractmethod

class DataImporter(ABC):
    def import_data(self):     # 模板方法
        data = self.open()
        records = self.parse(data)
        for r in records: self.save(r)
        self.close(data)
        self.after_import()

    @abstractmethod
    def open(self): ...
    @abstractmethod
    def parse(self, data): ...
    @abstractmethod
    def close(self, data): ...

    def save(self, r): db.save(r)
    def after_import(self): pass    # 钩子

TypeScript:abstract

abstract class DataImporter {
    importData() {
        const data = this.open();
        const records = this.parse(data);
        for (const r of records) this.save(r);
        this.close(data);
        this.afterImport();
    }

    protected abstract open(): any;
    protected abstract parse(data: any): Record[];
    protected abstract close(data: any): void;
    protected save(r: Record) { db.save(r); }
    protected afterImport() {}   // 钩子
}

Go:没有继承,用结构体嵌套 + 接口

// Go 没有"模板方法"的语法表达,常见折中:接口里塞一个回调
type Importer interface {
    Open() (io.Reader, error)
    Parse(r io.Reader) ([]Record, error)
    Close(r io.Reader) error
}

func RunImport(i Importer) error {
    r, err := i.Open()
    if err != nil { return err }
    defer i.Close(r)
    records, err := i.Parse(r)
    if err != nil { return err }
    for _, rec := range records { db.Save(rec) }
    return nil
}

模板方法的常见坑

坑 1:模板方法不 final,被子类覆盖。 子类覆盖了 importData 本身,整个骨架就废了。Java 里务必 final;Python / JS 没有 final,只能靠文档约定。

坑 2:钩子方法暴露过多内部细节。 钩子参数让子类访问到不该访问的内部对象,污染封装。规则:钩子参数尽量"窄",传值不传引用。

坑 3:继承层次过深。 父 → 子 → 孙子三层模板方法,孙子要同时满足父和子的契约,改一个都可能影响。建议继承层次不超过 2 层。

坑 4:替代 if 不彻底。 父类还在 importData 里写 if (csv) {...} else (json) {...}。这就退化成普通函数了。模板方法的精髓是用多态代替条件分支

坑 5:钩子被错误时机调用。 tearDown 应该在 runTest 抛异常时也执行,但简单实现里可能跳过。要用 try-finally 保证。

识别模板方法场景

  1. 多个类执行相似流程
  2. 流程骨架稳定,只有几个步骤可变
  3. 变体之间有公共代码
  4. 需要让用户/子类插入扩展逻辑(钩子)。

写在最后

模板方法是"框架"这个概念的祖师爷之一。它把"做事的顺序"固化在父类,把"具体怎么做每一步"开放给子类,这种"骨架不变、细节可填"的设计哲学贯穿所有大型软件。Spring、JUnit、Servlet 之所以让你的代码"插进去就能跑",就是因为它们用模板方法 + 钩子定义了清晰的扩展点。

给一个工程信号:当你发现自己在多个类里写几乎相同的流程,只有中间几步有差别时 —— 模板方法在向你招手。抽出父类,把变化点声明为抽象方法或钩子。代码会立刻清爽,且未来加新变体时"只填空,不动骨架"。在 Lambda 流行的今天,策略模式或许更轻量,但模板方法在"流程框架"领域仍然是不可替代的工具。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

策略模式完全指南:从 if/else 选算法到 Spring 注入与 AB 测试

2026-5-15 15:35:28

技术教程

访问者模式完全指南:从 AST 遍历到 Java NIO FileVisitor 的双分派艺术

2026-5-15 15:39:34

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索