别老写重复代码了!模版方法模式一次讲透

前言

你有没有遇到过这种情况------

团队里有几个功能,流程几乎一模一样:校验参数 → 查询数据 → 处理逻辑 → 返回结果。 但每个人各写各的,代码散落在四处,改一个公共逻辑要翻好几个文件,改完还容易漏。

这就是没有统一骨架的代价。

而今天要聊的模版方法模式(Template Method Pattern) ,正是专门解决这类问题的------ 把流程骨架锁在父类,把差异化步骤交给子类,既统一规范,又灵活扩展。


一、一句话定义

上层抽象类定义好操作的基本框架(骨架),某些特殊步骤交给子类实现。子类可以在不改变整体流程的前提下,定制其中某些步骤。

说白了就是:父类定规矩,子类填空格。


二、核心结构

模版方法模式只有两个角色,非常简洁:

scss 复制代码
AbstractClass(抽象类)
    ├── templateMethod()      ← 模版方法:定义流程骨架(final,不可覆盖)
    ├── method1()             ← 抽象方法:留给子类实现
    ├── method2()             ← 具体方法:基类默认实现,子类可覆盖
    └── method3()             ← 具体方法:基类默认实现
​
ConcreteClass(具体实现类)
    └── method1()             ← 实现父类定义的抽象步骤

关键点:

方法类型 定义位置 实现位置 说明
模版方法 抽象类 抽象类 流程骨架,建议加 final 防止被覆盖
抽象方法 抽象类 子类(必须) 差异化步骤,强制子类实现
具体方法 抽象类 抽象类(可被覆盖) 通用步骤,子类按需重写

三、代码实战------从简单到真实场景

3.1 基础版本

先看原始结构,理解骨架:

csharp 复制代码
/**
 * 抽象模版类:定义操作骨架
 */
public abstract class AbstractTemplate {
​
    // 模版方法:锁定流程,不允许子类覆盖
    public final void templateMethod() {
        method1();  // 抽象步骤,子类实现
        method2();  // 通用步骤,基类实现
        method3();  // 通用步骤,基类实现
    }
​
    // 抽象方法:差异化步骤,强制子类实现
    abstract void method1();
​
    // 具体方法:通用逻辑,子类可按需覆盖
    public void method2() {
        System.out.println("执行 method2... 基类实现");
    }
​
    public void method3() {
        System.out.println("执行 method3... 基类实现");
    }
}
scala 复制代码
/**
 * 具体实现类:只需实现差异化步骤
 */
public class ConcreteClass extends AbstractTemplate {
​
    @Override
    void method1() {
        System.out.println("子类实现特定步骤...");
    }
}

运行:

arduino 复制代码
public class Main {
    public static void main(String[] args) {
        AbstractTemplate template = new ConcreteClass();
        template.templateMethod();
    }
}

输出:

erlang 复制代码
子类实现特定步骤...
执行 method2... 基类实现
执行 method3... 基类实现

流程由父类控制,子类只管填自己的"空格"。


3.2 真实场景------数据导出功能

假设系统需要支持导出 ExcelCSV 两种格式,流程完全一样:

复制代码
查询数据 → 构建表头 → 填充数据行 → 生成文件

只有"构建表头"和"填充数据行"这两步格式不同,用模版方法处理再合适不过:

typescript 复制代码
/**
 * 数据导出模版
 */
public abstract class DataExportTemplate {
​
    // 模版方法:锁定导出流程
    public final void export(String fileName) {
        System.out.println("===== 开始导出:" + fileName + " =====");
​
        List<Object> data = queryData();       // 第一步:查询数据(通用)
        String header = buildHeader();         // 第二步:构建表头(子类定制)
        String content = buildContent(data);   // 第三步:填充内容(子类定制)
        writeFile(fileName, header, content);  // 第四步:写文件(通用)
​
        System.out.println("===== 导出完成 =====");
    }
​
    // 通用步骤:查询数据,所有格式一样
    private List<Object> queryData() {
        System.out.println("从数据库查询数据...");
        return List.of("row1", "row2", "row3"); // 模拟数据
    }
​
    // 抽象步骤:表头格式由子类决定
    protected abstract String buildHeader();
​
    // 抽象步骤:内容格式由子类决定
    protected abstract String buildContent(List<Object> data);
​
    // 通用步骤:写文件
    private void writeFile(String fileName, String header, String content) {
        System.out.println("写入文件:" + fileName);
        System.out.println("内容预览:\n" + header + "\n" + content);
    }
}
scala 复制代码
/**
 * Excel 导出实现
 */
public class ExcelExport extends DataExportTemplate {
​
    @Override
    protected String buildHeader() {
        return "ID\t姓名\t部门";  // Excel 用 Tab 分隔
    }
​
    @Override
    protected String buildContent(List<Object> data) {
        StringBuilder sb = new StringBuilder();
        data.forEach(row -> sb.append(row).append("\n"));
        return sb.toString();
    }
}
scala 复制代码
/**
 * CSV 导出实现
 */
public class CsvExport extends DataExportTemplate {
​
    @Override
    protected String buildHeader() {
        return "ID,姓名,部门";  // CSV 用逗号分隔
    }
​
    @Override
    protected String buildContent(List<Object> data) {
        StringBuilder sb = new StringBuilder();
        data.forEach(row -> sb.append(row).append("\r\n"));
        return sb.toString();
    }
}

调用:

ini 复制代码
DataExportTemplate excel = new ExcelExport();
excel.export("员工表.xlsx");
​
DataExportTemplate csv = new CsvExport();
csv.export("员工表.csv");

新增一种导出格式?只需继承 DataExportTemplate,实现两个方法,查询逻辑、写文件逻辑完全复用,一行公共代码都不用改。


四、你早就用过它------框架里的模版方法

模版方法模式在主流框架中随处可见,你可能已经用过很多次,只是没意识到:

Spring 中的 JdbcTemplate 查询 SQL 的流程(获取连接 → 执行 → 映射结果 → 关闭连接)是固定的,你只需实现 RowMapper 接口填充"映射结果"这一步。

Servlet 中的 HttpServlet service() 方法是模版方法,它根据请求类型分发到 doGet()doPost() 等------你只需覆盖你关心的方法。

MyBatis 中的 BaseExecutor SQL 执行流程由基类控制,缓存查询、结果处理等细节由子类实现。

Spring 生命周期回调 InitializingBean.afterPropertiesSet()@PostConstruct 本质上也是模版方法的变体------框架控制 Bean 初始化流程,你只填充自己的初始化逻辑。


五、优缺点 & 注意事项

优点

  • 消除重复代码:公共流程收归父类,一处修改处处生效
  • 强制规范:新增实现类必须遵循统一流程,不会乱来
  • 开闭原则:扩展新类型无需修改已有代码

缺点

  • 继承的局限:Java 是单继承,子类一旦继承了模版类,就无法再继承其他类
  • 流程不灵活:骨架固定,步骤顺序无法由子类调整(这正是它的特性,但有时也是约束)
  • 类数量膨胀:每种变体都需要一个子类,场景多了类会很多

注意事项

建议将模版方法声明为 final,明确告诉子类"流程骨架不可修改",防止被意外覆盖破坏整体逻辑。


六、与策略模式的一句话对比

很多人会混淆模版方法和策略模式,其实区分很简单:

模版方法模式 策略模式
实现方式 继承 组合
变化点 算法内部的某些步骤 整个算法
流程控制 父类控制 调用方控制
适用场景 流程固定,部分步骤可变 多种算法可随时切换

简单记:模版方法变"步骤",策略模式换"算法"。


总结

模版方法模式是设计模式里最接地气的一个,核心思想只有一句话:

把不变的部分抽到父类,把变化的部分留给子类。

当你发现几处代码流程高度相似、只有个别步骤不同时,就是它登场的时候了。

下次再遇到"复制粘贴改一改"的冲动,不妨停下来问自己:这里是不是可以用模版方法整一整?

相关推荐
数据中穿行1 小时前
建造者模式全方位深度解析
设计模式
是2的10次方啊1 小时前
String.format 替换踩坑记:从遇坑、读源码到手写实现
java·源码阅读
不光头强2 小时前
手写tomcat
java·tomcat
寻见9032 小时前
救命!Spring Boot 凭什么火?从道法术器讲透,新手也能一键上手
java·spring boot·java ee
jinanmichael2 小时前
【SQL】掌握SQL查询技巧:数据分组与排序
java·jvm·sql
彭于晏Yan2 小时前
SpringBoot如何调用节假日API
java·spring boot·后端
数据中穿行2 小时前
组合设计模式全方位深度解析
设计模式
jianfeng_zhu2 小时前
用java解决空心金字塔的问题
java·开发语言·python
数据中穿行2 小时前
原型设计模式全方位深度解析
设计模式