模板方法模式:抽象模板、具体子类、钩子方法

模板方法模式,说白了就是「把流程定死,把细节留给你」。我刚开始接触这个模式时,觉得它太简单了——不就是继承嘛。但后来在项目中用多了才发现,这玩意儿是框架设计的灵魂。

举个例子,你想想看:泡茶和泡咖啡,步骤是不是差不多?烧水、冲泡、倒杯、加料。但具体怎么冲泡、加什么料,又不一样。模板方法模式就是干这个的——把不变的流程封装在父类,把变化的细节交给子类。

模式结构:骨架与血肉

模板方法模式的核心角色就三个:

  • 抽象模板(AbstractClass):定义算法的骨架,包含一个或多个模板方法,以及若干基本操作。
  • 具体子类(ConcreteClass):实现父类定义的抽象基本操作,也可以重写钩子方法。
  • 钩子方法(Hook):父类中定义的默认实现,子类可以选择性重写,用来控制模板方法的执行流程。

这里我要强调一下钩子方法。很多初学者觉得钩子方法可有可无,其实不然。我在项目中遇到过这样一个场景:一个数据导出功能,大部分情况下需要生成表头,但某些特殊报表不需要。用钩子方法就完美解决了——父类默认生成表头,子类通过重写钩子方法决定是否调用。

核心思想:好莱坞原则——「不要打电话给我们,我们会打电话给你」。父类控制流程,子类提供实现。

代码示例:Java实现

先看一个简单的例子,模拟数据导出流程:

// 抽象模板
public abstract class DataExporter {
    // 模板方法,定义为final防止子类修改流程
    public final void export() {
        openFile();
        writeHeader();  // 钩子方法
        writeData();
        writeFooter();
        closeFile();
    }
    
    protected void openFile() {
        System.out.println("打开文件");
    }
    
    // 钩子方法:默认实现,子类可重写
    protected void writeHeader() {
        System.out.println("写入默认表头");
    }
    
    protected abstract void writeData();
    
    protected void writeFooter() {
        System.out.println("写入页脚");
    }
    
    protected void closeFile() {
        System.out.println("关闭文件");
    }
}

// 具体子类:CSV导出
public class CsvExporter extends DataExporter {
    @Override
    protected void writeData() {
        System.out.println("写入CSV格式数据");
    }
    
    // 不重写writeHeader,使用默认表头
}

// 具体子类:PDF导出,不需要表头
public class PdfExporter extends DataExporter {
    @Override
    protected void writeData() {
        System.out.println("写入PDF格式数据");
    }
    
    @Override
    protected boolean shouldWriteHeader() {
        return false;  // PDF不需要表头
    }
}
我的习惯:模板方法尽量声明为final,防止子类不小心重写破坏了流程。钩子方法用protected,子类可见但外部不可见。

实战案例:JdbcTemplate

说到模板方法模式,Spring的JdbcTemplate是个经典案例。我当年看源码时,真是拍案叫绝。

JdbcTemplate的核心流程是固定的:获取连接、创建语句、执行查询、处理结果集、清理资源。但具体怎么处理结果集,是千变万化的。Spring用模板方法模式完美解决了这个问题:

// JdbcTemplate的核心模板方法
public <T> T query(String sql, ResultSetExtractor<T> rse) throws DataAccessException {
    Connection con = null;
    Statement stmt = null;
    ResultSet rs = null;
    try {
        con = getConnection();
        stmt = con.createStatement();
        rs = stmt.executeQuery(sql);
        return rse.extractData(rs);  // 模板方法中的抽象步骤
    } catch (SQLException ex) {
        throw new DataAccessException(ex);
    } finally {
        closeResultSet(rs);
        closeStatement(stmt);
        closeConnection(con);
    }
}

你发现没有?JdbcTemplate把「获取连接-执行查询-清理资源」这个固定流程封装好了,你只需要实现ResultSetExtractor接口,告诉它怎么处理结果集就行。这就是模板方法模式的精髓——把脏活累活都干了,你只关心业务逻辑。

避坑指南:我曾经在一个项目中,有人试图重写JdbcTemplate的模板方法,结果搞得一团糟。记住:模板方法模式中,父类的方法不要轻易重写,要改就改钩子方法或者抽象方法。

实战案例:Servlet生命周期

另一个经典案例是Servlet的生命周期。每个Java Web开发者都接触过:

public interface Servlet {
    void init(ServletConfig config);     // 初始化
    void service(ServletRequest req, ServletResponse res);  // 服务
    void destroy();                      // 销毁
    ServletConfig getServletConfig();
    String getServletInfo();
}

Servlet容器(比如Tomcat)会按照固定顺序调用这些方法:先init,再service,最后destroy。你写的HttpServlet只需要重写doGet、doPost这些方法就行。这就是模板方法模式在框架层面的应用。

我记得有一次排查一个内存泄漏问题,发现是某个Servlet在init方法中开了线程池,但destroy方法没关。这就是典型的「只重写了模板方法的一部分,忘了另一部分」。所以我说,用模板方法模式时,一定要把「清理」也考虑进去。

钩子方法的妙用

钩子方法,说白了就是「给你一个机会插手」。父类留个口子,子类可以选择要不要干预流程。

举个例子,一个游戏角色升级系统:

public abstract class Character {
    public final void levelUp() {
        increaseLevel();
        increaseStats();
        if (shouldLearnNewSkill()) {  // 钩子方法
            learnNewSkill();
        }
        showLevelUpEffect();
    }
    
    protected boolean shouldLearnNewSkill() {
        return true;  // 默认每级都学新技能
    }
    
    protected abstract void increaseStats();
    // ... 其他方法
}

public class Warrior extends Character {
    @Override
    protected void increaseStats() {
        // 增加力量和体力
    }
    
    @Override
    protected boolean shouldLearnNewSkill() {
        return getLevel() % 2 == 0;  // 战士每两级学一次新技能
    }
}

你看,钩子方法让子类有了「选择权」,但又不会破坏父类定义的流程。我在做游戏后端时,这种设计用得特别多——不同的职业、不同的角色,共享一套升级流程,但细节各不相同。

SVG结构图:模板方法模式核心逻辑

模板方法模式核心结构 抽象模板(AbstractClass) + templateMethod() : final # primitiveOperation1() : abstract # primitiveOperation2() : abstract # hookMethod() : 默认实现 # concreteOperation() : 已实现 具体子类A(ConcreteClassA) + primitiveOperation1() : 实现 + primitiveOperation2() : 实现 + hookMethod() : 重写(可选) 具体子类B(ConcreteClassB) + primitiveOperation1() : 实现 + primitiveOperation2() : 实现 + hookMethod() : 不重写(默认) 继承 继承 模板方法(templateMethod)定义算法骨架,子类通过实现抽象方法提供具体行为。 钩子方法(hookMethod)提供默认实现,子类可选择性重写以影响流程。

模板方法模式的优缺点

优点 缺点
代码复用:公共行为放在父类,避免重复 类数量增加:每个实现都需要一个子类
扩展性好:新增子类即可扩展功能 继承关系强:父类修改可能影响所有子类
控制反转:父类控制流程,子类提供细节 违反里氏替换原则?不,只要设计得当就不会
符合开闭原则:对扩展开放,对修改关闭 调试时可能不太直观,需要理解父类流程
什么时候用模板方法模式?
  • 多个子类有相同的算法步骤,但具体实现不同
  • 你想控制子类的扩展点,防止它们破坏整体流程
  • 框架设计中,让用户只关注业务逻辑,不关心底层流程

避坑指南:我踩过的三个坑

第一个坑:我曾经在一个项目中,把模板方法设计得过于复杂,里面嵌套了七八个钩子方法。结果子类重写时,根本搞不清哪些钩子会影响哪些步骤。后来我学乖了——钩子方法不要超过三个,而且每个钩子只控制一个明确的决策点。

第二个坑:模板方法里调用了子类的方法,但子类的方法又间接调回了父类的模板方法。嗯,死循环了。这就是所谓的「模板方法反模式」。记住:子类实现的方法中,不要调用父类的模板方法。

第三个坑:有人把模板方法设计成非final的,然后子类重写了整个模板方法。这完全违背了模板方法模式的初衷。如果你发现子类需要重写整个流程,那说明这个模式不适合你的场景,换个策略吧。

我的建议:模板方法模式最适合「80%相同,20%不同」的场景。如果子类之间的差异超过50%,考虑用策略模式或者组合模式替代。

好了,关于模板方法模式就聊这么多。这个模式看起来简单,但用好了能让你的代码结构清晰、扩展方便。下次写框架或者公共组件时,不妨想想——能不能用模板方法模式把固定流程封装起来?


公众号:蓝海资料掘金营,微信deep3321