代码重构: 用实际的例子去讲解模版方法

6 阅读2分钟

模板方法模式

在很多业务代码中,我们都会遇到这种场景:

  • 整体流程是固定的
  • 但流程中的某些步骤因业务类型不同而不同
  • 又不希望子类随意修改流程顺序

如果你把这些逻辑全部写进一个类里,往往会出现:

  • 类越来越大
  • if / switch 越来越多
  • 改一处逻辑容易影响其他场景

这正是 模板方法模式 要解决的问题。


一、问题从哪里来?

一开始,我有一个用于“保存代码文件”的工具类,大概长这样:

  • 校验参数
  • 创建唯一目录
  • 把代码写成文件
  • 返回目录路径

当前代码存在不同的报错逻辑:

  • HTML 单文件是一套保存逻辑
  • HTML + CSS + JS 又是一套保存逻辑
  • 所有逻辑都堆在一个类里,越来越臃肿

导致:
这个类在不断变大,而且每加一种类型就要改原有代码。


二、什么是不变的?

整理之后会发现:

不变的部分:

  1. 保存流程是固定的
  2. 都要校验参数
  3. 都要创建目录
  4. 最后都返回一个目录

变化的部分:

  • 写哪些文件
  • 每个文件的内容来自哪里

这正好符合一句模版方法的逻辑:

流程固定,具体实现内容待定。


三、模板方法模式的核心想法

模板方法模式其实很简单:

把“流程”放在父类,把“变化”交给子类。

父类只做一件事:
规定顺序,不让子类乱来。


四、一个简化后的模板类

public abstract class CodeFileSaverTemplate<T> {

    public final File saveCode(T result) {
        validate(result);
        String dir = createDir();
        saveFiles(result, dir);
        return new File(dir);
    }

    protected void validate(T result) {}

    protected abstract void saveFiles(T result, String dir);

    protected String createDir() {
        
        return dir;
    }
}

这里有三个关键信号:

  • saveCode()final:流程不能被改【固定的模版流程】
  • protected:只给子类用
  • abstract:子类必须实现

五、子类只关心自己具体实现

HTML 保存

public class HtmlSaver extends CodeFileSaverTemplate<HtmlCodeResult> {

    @Override
    protected void saveFiles(HtmlCodeResult result, String dir) {
        write(dir, "index.html", result.getHtmlCode());
    }
}

多文件保存

public class MultiFileSaver extends CodeFileSaverTemplate<MultiFileCodeResult> {

    @Override
    protected void saveFiles(MultiFileCodeResult result, String dir) {
        write(dir, "index.html", result.getHtmlCode());
        write(dir, "style.css", result.getCssCode());
        write(dir, "script.js", result.getJsCode());
    }
}

子类不需要关心:

  • 目录怎么建
  • 校验顺序
  • 返回值

只管一件事:
我要写哪些文件。


六、为什么不用 if / switch?

当然可以写成这样:

if (type == HTML) { ... }
else if (type == MULTI) { ... }

但问题是:

  • 每加一种类型就要改这个类
  • 老逻辑和新逻辑混在一起
  • 长期一定失控

模板方法的好处是:

新增一种类型 = 新增一个类,不动旧代码。


七、什么时候该用模板方法?

适合用在:

  • 流程天然有顺序
  • 顺序不允许被破坏
  • 变化点明确、可控

不适合用在:

  • 流程差异非常大
  • 需要频繁运行时切换逻辑
  • 不想使用继承的场景