模板方法模式:抽象模板、具体子类、钩子方法
模板方法模式,说白了就是「把流程定死,把细节留给你」。我刚开始接触这个模式时,觉得它太简单了——不就是继承嘛。但后来在项目中用多了才发现,这玩意儿是框架设计的灵魂。
举个例子,你想想看:泡茶和泡咖啡,步骤是不是差不多?烧水、冲泡、倒杯、加料。但具体怎么冲泡、加什么料,又不一样。模板方法模式就是干这个的——把不变的流程封装在父类,把变化的细节交给子类。
模式结构:骨架与血肉
模板方法模式的核心角色就三个:
- 抽象模板(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不需要表头
}
}
实战案例: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接口,告诉它怎么处理结果集就行。这就是模板方法模式的精髓——把脏活累活都干了,你只关心业务逻辑。
实战案例: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结构图:模板方法模式核心逻辑
模板方法模式的优缺点
| 优点 | 缺点 |
|---|---|
| 代码复用:公共行为放在父类,避免重复 | 类数量增加:每个实现都需要一个子类 |
| 扩展性好:新增子类即可扩展功能 | 继承关系强:父类修改可能影响所有子类 |
| 控制反转:父类控制流程,子类提供细节 | 违反里氏替换原则?不,只要设计得当就不会 |
| 符合开闭原则:对扩展开放,对修改关闭 | 调试时可能不太直观,需要理解父类流程 |
- 多个子类有相同的算法步骤,但具体实现不同
- 你想控制子类的扩展点,防止它们破坏整体流程
- 框架设计中,让用户只关注业务逻辑,不关心底层流程
避坑指南:我踩过的三个坑
第一个坑:我曾经在一个项目中,把模板方法设计得过于复杂,里面嵌套了七八个钩子方法。结果子类重写时,根本搞不清哪些钩子会影响哪些步骤。后来我学乖了——钩子方法不要超过三个,而且每个钩子只控制一个明确的决策点。
第二个坑:模板方法里调用了子类的方法,但子类的方法又间接调回了父类的模板方法。嗯,死循环了。这就是所谓的「模板方法反模式」。记住:子类实现的方法中,不要调用父类的模板方法。
第三个坑:有人把模板方法设计成非final的,然后子类重写了整个模板方法。这完全违背了模板方法模式的初衷。如果你发现子类需要重写整个流程,那说明这个模式不适合你的场景,换个策略吧。
好了,关于模板方法模式就聊这么多。这个模式看起来简单,但用好了能让你的代码结构清晰、扩展方便。下次写框架或者公共组件时,不妨想想——能不能用模板方法模式把固定流程封装起来?
公众号:蓝海资料掘金营,微信deep3321