31、重构与设计模式(四):用模板方法模式重构重复算法

说实话,做重构这么多年,我见过最多的重复代码不是那种简单的复制粘贴,而是「流程相同,细节不同」的算法骨架。你想想看,两个方法长得像双胞胎,但中间某几步的实现完全不一样。这种重复最坑人——改一个地方,另一个忘了改,线上就炸了。

今天聊的模板方法模式,就是专门治这种病的。

什么是模板方法模式?

说白了,就是把算法的骨架固定下来,把可变的部分留给子类去实现。父类定义「怎么做」,子类决定「做什么」。

举个例子,我早期维护过一个报表系统。每天要生成日报、周报、月报,流程都是:

  1. 查询原始数据
  2. 做数据清洗
  3. 计算统计指标
  4. 生成报表文件

你看,步骤一模一样。但日报查的是当天数据,月报查的是整月数据。清洗规则也不同,日报要过滤掉无效记录,月报还要做数据聚合。如果每个报表都写一遍完整流程,那代码量直接翻三倍。

核心思想:把不变的部分封装在父类,把变化的部分抽象成方法,让子类去覆盖。

重构前的代码:重复的算法

先看看重构前的样子。这是日报生成器:

public class DailyReportGenerator {
    public void generate() {
        // 1. 查询数据
        List<Data> rawData = queryDailyData();
        
        // 2. 清洗数据
        List<Data> cleaned = cleanDailyData(rawData);
        
        // 3. 计算指标
        Report report = calculateDailyMetrics(cleaned);
        
        // 4. 生成文件
        saveReport(report, "daily");
    }
    
    private List<Data> queryDailyData() {
        // 查询当天数据
    }
    
    private List<Data> cleanDailyData(List<Data> data) {
        // 过滤无效记录
    }
    
    private Report calculateDailyMetrics(List<Data> data) {
        // 计算日报指标
    }
}

这是月报生成器:

public class MonthlyReportGenerator {
    public void generate() {
        // 1. 查询数据
        List<Data> rawData = queryMonthlyData();
        
        // 2. 清洗数据
        List<Data> cleaned = cleanMonthlyData(rawData);
        
        // 3. 计算指标
        Report report = calculateMonthlyMetrics(cleaned);
        
        // 4. 生成文件
        saveReport(report, "monthly");
    }
    
    private List<Data> queryMonthlyData() {
        // 查询整月数据
    }
    
    private List<Data> cleanMonthlyData(List<Data> data) {
        // 过滤 + 聚合
    }
    
    private Report calculateMonthlyMetrics(List<Data> data) {
        // 计算月报指标
    }
}

两个类里,generate() 方法的骨架完全一样。只是每个步骤调用的具体方法不同。这种重复,我称之为「流程级重复」——比代码级重复更隐蔽,危害也更大。

注意:流程级重复的典型特征是——方法签名不同,但调用顺序和结构完全一致。如果你发现两个方法里都有「先A后B再C」的结构,那大概率可以用模板方法模式。

重构后:模板方法模式登场

重构的思路很简单:把公共的流程提取到父类,把变化的步骤定义为抽象方法。

public abstract class ReportGenerator {
    
    // 模板方法:定义算法骨架
    public final void generate() {
        List<Data> rawData = queryData();
        List<Data> cleaned = cleanData(rawData);
        Report report = calculateMetrics(cleaned);
        saveReport(report);
    }
    
    // 抽象方法:子类必须实现
    protected abstract List<Data> queryData();
    protected abstract List<Data> cleanData(List<Data> rawData);
    protected abstract Report calculateMetrics(List<Data> cleaned);
    
    // 钩子方法:可选覆盖
    protected void saveReport(Report report) {
        // 默认实现:保存到本地文件
        System.out.println("保存报表:" + report.getName());
    }
}

然后子类只需要关注差异部分:

public class DailyReportGenerator extends ReportGenerator {
    
    @Override
    protected List<Data> queryData() {
        return queryDailyData();
    }
    
    @Override
    protected List<Data> cleanData(List<Data> rawData) {
        return cleanDailyData(rawData);
    }
    
    @Override
    protected Report calculateMetrics(List<Data> cleaned) {
        return calculateDailyMetrics(cleaned);
    }
    
    private List<Data> queryDailyData() {
        // 查询当天数据
    }
    
    private List<Data> cleanDailyData(List<Data> data) {
        // 过滤无效记录
    }
    
    private Report calculateDailyMetrics(List<Data> data) {
        // 计算日报指标
    }
}
public class MonthlyReportGenerator extends ReportGenerator {
    
    @Override
    protected List<Data> queryData() {
        return queryMonthlyData();
    }
    
    @Override
    protected List<Data> cleanData(List<Data> rawData) {
        return cleanMonthlyData(rawData);
    }
    
    @Override
    protected Report calculateMetrics(List<Data> cleaned) {
        return calculateMonthlyMetrics(cleaned);
    }
    
    private List<Data> queryMonthlyData() {
        // 查询整月数据
    }
    
    private List<Data> cleanMonthlyData(List<Data> data) {
        // 过滤 + 聚合
    }
    
    private Report calculateMonthlyMetrics(List<Data> data) {
        // 计算月报指标
    }
}

你看,重复的 generate() 方法消失了。每个子类只写自己独特的那部分。如果以后要加周报,只需要再继承一个 WeeklyReportGenerator,实现三个抽象方法就行。

个人经验:我习惯把模板方法声明为 final,防止子类不小心覆盖了骨架。抽象方法用 protected,既能让子类访问,又不暴露给外部调用者。

钩子方法:让模板更灵活

有时候,子类并不需要实现所有步骤。比如某些报表不需要数据清洗,直接拿原始数据算指标就行。这时候可以用「钩子方法」——父类提供一个默认实现,子类按需覆盖。

public abstract class ReportGenerator {
    
    public final void generate() {
        List<Data> rawData = queryData();
        
        // 钩子方法:是否需要清洗
        if (needClean()) {
            rawData = cleanData(rawData);
        }
        
        Report report = calculateMetrics(rawData);
        saveReport(report);
    }
    
    protected abstract List<Data> queryData();
    
    // 钩子方法:默认需要清洗
    protected boolean needClean() {
        return true;
    }
    
    protected List<Data> cleanData(List<Data> rawData) {
        // 默认清洗逻辑
        return rawData;
    }
    
    protected abstract Report calculateMetrics(List<Data> cleaned);
    
    protected void saveReport(Report report) {
        System.out.println("保存报表:" + report.getName());
    }
}

如果某个报表不需要清洗,子类只需要覆盖 needClean() 返回 false 即可:

public class RawReportGenerator extends ReportGenerator {
    
    @Override
    protected List<Data> queryData() {
        // 查询原始数据
    }
    
    @Override
    protected boolean needClean() {
        return false;  // 不需要清洗
    }
    
    @Override
    protected Report calculateMetrics(List<Data> data) {
        // 直接计算
    }
}

避坑指南:我曾经在一个项目里滥用钩子方法,加了七八个 needXxx() 判断。结果子类里全是 return false,代码反而更乱了。钩子方法控制在两三个以内,超过这个数,说明你的模板设计有问题。

模板方法模式的适用场景

不是所有重复都适合用模板方法。我总结了几条判断标准:

场景 适合模板方法? 原因
多个类有相同的算法骨架 ✅ 非常适合 骨架固定,细节可变
算法步骤固定,但实现不同 ✅ 非常适合 模板方法就是为此而生
只有一两处代码不同 ⚠️ 谨慎使用 可能过度设计,用策略模式更轻量
算法步骤本身也在变化 ❌ 不适合 骨架不稳定,模板方法会频繁修改

模板方法 vs 策略模式

这两个模式经常被拿来比较。我个人的区分方法是:

  • 模板方法:控制算法的「骨架」,子类只能填充细节
  • 策略模式:控制算法的「整体」,运行时可以替换整个算法

举个例子,报表生成用模板方法,因为流程是固定的。但报表的「保存方式」可以用策略模式——今天存本地文件,明天存数据库,后天发邮件。保存方式变了,但生成流程不变。

你想想看,如果我把保存方式也做成抽象方法,那每次新增保存方式都要新建一个子类,类数量会爆炸。用策略模式组合进去,就灵活多了。

核心区别:模板方法是「继承」的思维,策略模式是「组合」的思维。能用组合就别用继承,这是设计模式的老规矩。

重构收益总结

用模板方法模式重构重复算法,带来的好处很明显:

  1. 消除重复:公共流程只写一次,改一次全改
  2. 统一规范:所有子类必须遵循相同的步骤,不会有人漏掉某一步
  3. 扩展方便:新增一种报表,只需要写三个方法,不用重写整个流程
  4. 测试简单:父类的流程可以单独测试,子类只需要测试自己的差异逻辑

我记得有一次,团队要紧急上线一种新的统计报表。我花十分钟写了一个子类,实现了三个抽象方法,测试通过直接上线。旁边的同事还在复制粘贴改代码,改到一半发现漏了一个步骤……嗯,从那以后,我们团队就把模板方法模式列入了「必用模式」清单。

模板方法模式:重构重复算法 ReportGenerator(抽象父类) + generate() 【模板方法】 # queryData() 【抽象】 # cleanData() 【抽象】 # calculateMetrics() 【抽象】 # saveReport() 【钩子】 DailyReportGenerator MonthlyReportGenerator WeeklyReportGenerator

这张图很直观:父类定义了完整的流程骨架,子类只负责填充抽象方法。钩子方法给了子类一定的灵活性,但整体流程依然由父类控制。

最后说一句,模板方法模式不是银弹。如果你的算法步骤经常变化,或者子类之间的差异远大于共性,那强行用模板方法只会让代码更僵化。重构的本质是「对症下药」,模式只是工具,别为了用模式而用模式。


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