模板方法设计模式 | JavaSE

模板方法设计模式

一、学习目标

学完本章,你应该能够:

  • 解释模板方法设计模式(Template Method Pattern)解决什么问题。
  • 理解“固定流程”与“可变步骤”的区别。
  • 能够从多个高度重复的方法中识别公共流程。
  • 能够使用抽象父类封装公共算法骨架。
  • 能够把不同子类的变化步骤抽取为抽象方法。
  • 能够让模板方法统一调用抽象步骤。
  • 理解为什么子类只需要关注自己的差异部分。
  • 理解为什么模板方法经常使用 final 修饰。
  • 理解模板方法与普通代码复用之间的区别。
  • 理解模板方法如何结合抽象类、继承、重写和多态。
  • 能够独立完成 DataImporter 模板案例。
  • 能够判断什么时候适合使用模板方法,什么时候只是普通方法抽取。

二、核心知识

2.1 从重复代码开始

假设现在需要完成两种数据导入:

CSV 数据导入
Excel 数据导入

CSV:

public void importCsv() {

    System.out.println("读取文件");

    System.out.println(
            "校验文件格式"
    );

    System.out.println(
            "转换 CSV 数据"
    );

    System.out.println(
            "保存到数据库"
    );
}

Excel:

public void importExcel() {

    System.out.println("读取文件");

    System.out.println(
            "校验文件格式"
    );

    System.out.println(
            "转换 Excel 数据"
    );

    System.out.println(
            "保存到数据库"
    );
}

对比:

读取文件                 相同
校验文件格式             相同

转换 CSV / Excel 数据    不同

保存到数据库             相同

真正重复的是:

整个流程结构
+
大部分固定步骤

只有:

数据转换方式

发生变化。

这种情况下,如果继续复制粘贴整个方法:

CSV 一份
Excel 一份
JSON 一份
XML 一份
...

代码重复会越来越严重。


2.2 最简单的普通方法抽取

第一反应可能是:

public void read() {
    System.out.println("读取文件");
}

public void validate() {
    System.out.println("校验数据");
}

public void save() {
    System.out.println("保存数据");
}

这样确实减少了一部分重复。

但是每个子类仍然可能自己写:

read();
validate();
convert();
save();

问题仍然存在:

每个子类都要重复写流程

而且可能有人写成:
validate();
save();
convert();
read();

如果业务要求流程必须固定:

读取
↓
校验
↓
转换
↓
保存

仅仅抽取几个公共方法还不够。

我们需要进一步:

把整个流程本身也统一起来。


2.3 什么是模板方法设计模式

模板方法设计模式英文:

Template Method Pattern

当前课程可以这样理解:

在父类中定义完成某项工作的固定流程骨架,把所有实现共有的步骤写在父类中,把部分需要变化的步骤留给子类实现。

基本结构:

抽象父类
│
├── templateMethod()
│      固定整体流程
│
├── commonStep1()
│      公共实现
│
├── abstract variableStep()
│      子类提供实现
│
└── commonStep2()
       公共实现

子类:

ChildA
└── variableStep() A方案

ChildB
└── variableStep() B方案

2.4 原课程给出的核心写法

原教学资料把模板方法模式总结成:

1. 定义一个抽象类

2. 定义模板方法
   放公共、固定流程

3. 定义抽象方法
   把具体不同实现交给子类

4. 建议使用 final 修饰模板方法

这是本章最核心结构。


2.5 第一个模板方法案例

public abstract class DataImporter {

    public final void importData() {

        read();

        validate();

        convert();

        save();
    }

    private void read() {
        System.out.println(
                "读取文件"
        );
    }

    private void validate() {
        System.out.println(
                "校验文件"
        );
    }

    protected abstract void convert();

    private void save() {
        System.out.println(
                "保存到数据库"
        );
    }
}

这里:

importData()

就是:

模板方法。

它定义了:

读取
↓
校验
↓
转换
↓
保存

整个算法骨架。


2.6 哪些步骤是固定步骤

在:

importData()

中:

read();
validate();
save();

如果所有导入器都完全一样,就可以直接由父类实现。

例如:

private void read() {
    System.out.println(
            "读取文件"
    );
}

这种步骤:

不希望每个子类重复实现,也不需要每个子类变化。

可以称为:

固定步骤
公共步骤

2.7 哪些步骤是可变步骤

protected abstract void convert();

父类知道:

所有数据导入都必须执行“数据转换”。

但是:

CSV 怎么转换
Excel 怎么转换
JSON 怎么转换

并不相同。

所以:

convert()

只定义行为:

protected abstract void convert();

具体实现交给子类。

这就是:

变化点。


2.8 CSVImporter

public class CsvImporter
        extends DataImporter {

    @Override
    protected void convert() {
        System.out.println(
                "转换 CSV 数据"
        );
    }
}

子类只需要关心:

CSV 和别人哪里不一样?

答案:

convert()

因此只实现这一部分。


2.9 ExcelImporter

public class ExcelImporter
        extends DataImporter {

    @Override
    protected void convert() {
        System.out.println(
                "转换 Excel 数据"
        );
    }
}

它同样不需要重新写:

读取
校验
保存

也不需要重复定义:

整个 importData 流程

2.10 调用模板方法

测试:

public class Test {

    public static void main(String[] args) {

        DataImporter csv =
                new CsvImporter();

        csv.importData();

        System.out.println(
                "---------------"
        );

        DataImporter excel =
                new ExcelImporter();

        excel.importData();
    }
}

对于:

csv.importData();

流程:

DataImporter.importData()
        ↓
read()
        ↓
validate()
        ↓
convert()
        ↓
CsvImporter.convert()
        ↓
save()

而:

excel.importData();

流程:

DataImporter.importData()
        ↓
read()
        ↓
validate()
        ↓
convert()
        ↓
ExcelImporter.convert()
        ↓
save()

整个算法骨架没有变化。

真正变化的只是:

convert()

2.11 为什么模板方法使用 final

原课程特别强调:

建议使用 final 修饰模板方法。

例如:

public final void importData() {

    read();
    validate();
    convert();
    save();
}

为什么?

因为整个模板方法存在的目的就是:

固定算法骨架。

如果子类可以:

@Override
public void importData() {

    save();
    convert();
    read();
}

那父类规定的:

读取
↓
校验
↓
转换
↓
保存

就被彻底破坏了。

所以在当前课程这种:

流程必须严格固定

的模板方法中:

final

非常合理。


2.12 final 真正保护的是什么

不是为了:

“看到模板方法就机械加 final”

而是因为当前设计明确要求:

子类只能改变指定步骤,不能重新定义整个算法流程。

所以:

final templateMethod()

真正保护的是:

算法骨架。


2.13 模板方法模式中的“变与不变”

这是本章最重要的设计思想。

分析一个业务流程:

哪些永远一样?
        ↓
放父类固定

哪些一定存在,但实现不同?
        ↓
定义抽象方法
        ↓
交给子类

例如:

文章生成流程

初始化             固定
读取数据           固定
生成正文           变化
格式检查           固定
保存结果           固定

就可以考虑:

template()
├── init()
├── read()
├── generate()   ← abstract
├── check()
└── save()

2.14 Template Method 和抽象类是什么关系

模板方法模式通常会利用:

抽象类
+
抽象方法
+
继承
+
方法重写

父类:

定义流程骨架

子类:

实现变化步骤

因此上一章:

抽象类与抽象方法

实际上就是本章的重要语言基础。


2.15 Template Method 与多态是什么关系

父类模板方法内部:

convert();

表面写的是:

DataImporter 中定义的抽象行为

实际对象如果是:

CsvImporter

就运行:

CsvImporter.convert()

实际对象如果是:

ExcelImporter

就运行:

ExcelImporter.convert()

所以模板方法内部本身就在利用:

方法重写 + 动态分派。


2.16 Template Method 调用关系

可以画成:

调用者
  │
  ▼
templateMethod()
  │
  ├── commonStep()
  │
  ├── abstractStep()
  │       │
  │       ├── ChildA 实现
  │       └── ChildB 实现
  │
  └── commonStep()

最有意思的地方是:

父类代码在运行过程中主动调用子类提供的实现。

这与普通理解中的:

子类调用父类方法

方向不同。


2.17 “父类调用子类实现”怎么做到的

代码:

DataImporter importer =
        new CsvImporter();

importer.importData();

进入:

DataImporter.importData()

模板中:

convert();

因为当前实际对象:

CsvImporter

所以动态分派最终找到:

CsvImporter.convert()

因此实际调用链:

父类模板方法
    ↓
调用抽象步骤
    ↓
运行时选择子类实现

这就是多态真正参与模板方法的地方。


2.18 模板方法解决的不只是“重复几行代码”

原资料把它总结为:

解决方法中的重复代码问题。

这是最直接的效果。

但进一步看,它还解决:

1. 公共代码重复

2. 流程顺序容易被多个子类写乱

3. 公共流程修改时需要修改很多类

4. 子类承担了不必要的流程控制责任

模板方法把:

流程控制权

集中到父类。

子类只负责:

变化部分

三、使用方法

3.1 DataImporter 完整版

public abstract class DataImporter {

    public final void importData() {

        read();

        validate();

        convert();

        save();
    }

    private void read() {
        System.out.println(
                "1. 读取原始数据"
        );
    }

    private void validate() {
        System.out.println(
                "2. 校验原始数据"
        );
    }

    protected abstract void convert();

    private void save() {
        System.out.println(
                "4. 保存处理结果"
        );
    }
}

CSV:

public class CsvImporter
        extends DataImporter {

    @Override
    protected void convert() {
        System.out.println(
                "3. 转换 CSV 数据"
        );
    }
}

Excel:

public class ExcelImporter
        extends DataImporter {

    @Override
    protected void convert() {
        System.out.println(
                "3. 转换 Excel 数据"
        );
    }
}

3.2 为什么 fixed step 可以 private

例如:

private void read()

如果设计明确规定:

子类不需要修改读取规则。

那么甚至可以:

private

完全隐藏给父类自己使用。

这样子类只能看到真正的扩展点:

protected abstract void convert();

这使 API 更清楚:

你应该改哪里?
        ↓
convert()

你不应该改哪里?
        ↓
read / validate / save

3.3 protected abstract 作为扩展点

为什么:

protected abstract void convert();

而不是:

public abstract void convert();

如果 convert() 只是:

模板内部调用、供子类实现的内部步骤

那么没必要把它公开给整个系统。

使用:

protected

可以表达:

这是继承体系内部的扩展点。

这正好与前面学习的权限修饰符结合起来。


3.4 学生成绩统计案例

假设不同班级都执行:

读取学生数据
↓
统计平均分
↓
执行自定义统计
↓
打印完成

其中:

自定义统计

不同。

可以:

public abstract class StatisticsTask {

    public final void execute() {

        readStudents();

        calculateAverage();

        customStatistics();

        finish();
    }

    private void readStudents() {
        System.out.println(
                "读取学生数据"
        );
    }

    private void calculateAverage() {
        System.out.println(
                "计算平均分"
        );
    }

    protected abstract void customStatistics();

    private void finish() {
        System.out.println(
                "统计完成"
        );
    }
}

四、原理与进阶

4.1 模板方法是一种“控制反转”的雏形

普通调用:

子类
↓
调用父类工具方法

Template Method:

外部
↓
调用父类模板
↓
父类控制整个流程
↓
父类在指定位置调用子类实现

也就是:

总体控制权在父类,子类只提供变化步骤。

这种思想以后学习:

框架
Spring
回调
插件式设计

时会不断遇到。

当前阶段只需要理解这种方向变化,不需要提前学习框架。


4.2 好莱坞原则的直觉

模板方法常被用一个形象思想描述:

不要调用我们,我们会调用你。

这里不是让子类自己控制:

什么时候执行 read
什么时候执行 convert
什么时候执行 save

而是:

父类模板

决定时间和顺序。

子类只提供:

convert 的具体实现

这就是框架式设计的一种非常早期的直觉。


4.3 不是所有重复代码都需要 Template Method

例如:

public int add(
        int a,
        int b
) {
    return a + b;
}

三个类都需要加法。

最自然可能只是:

抽取公共方法

没有必要:

创建抽象类
创建继承体系
创建抽象步骤

模板方法真正适合的是:

存在稳定整体流程
+
多个实现共享流程结构
+
部分步骤存在可预测变化

4.4 为什么不能为了模式而模式

如果只有:

一个实现类

而且流程未来也没有变化需求,却设计:

AbstractProcessor
BaseProcessor
TemplateProcessor
ConcreteProcessor

可能只是增加复杂度。

所以设计模式最重要的不是:

“我用了 Template Method,很高级。”

而是:

当前问题真的具有固定流程与可变步骤吗?


4.5 模板方法与普通父类复用的区别

普通父类复用可能只是:

protected void read() {
}

子类自行:

read();
convert();
save();

而模板方法进一步:

public final void execute() {

    read();

    convert();

    save();
}

区别在于:

普通复用:
只复用单个功能

模板方法:
连整个算法执行骨架也统一

4.6 模板方法不一定只有一个抽象步骤

简单教材结构:

模板方法
+
一个抽象方法

只是最基础版本。

实际可以:

public final void execute() {

    read();

    validate();

    transform();

    save();
}

其中:

protected abstract void validate();

protected abstract void transform();

两个步骤都由不同子类实现。

关键不是抽象方法数量,而是:

哪些步骤稳定,哪些步骤变化。


4.7 模板方法本身是不是绝对必须 final

在原课程当前结构中:

建议使用 final。

原因非常明确:

防止子类重写后破坏算法骨架。

因此本教程训练代码都采用:

public final void templateMethod()

但是从更广义的设计模式讨论看,是否强制 final 与具体框架设计有关。

当前课程阶段最合理的掌握方式是:

当整个流程不允许子类改变时,使用 final 明确保护模板方法。

不要机械背成:

世界上所有叫“模板方法”的方法语法上都必须 final。


4.8 可变步骤为什么设计成 abstract

如果写:

protected void convert() {
}

空实现。

子类可能忘记重写。

程序依然编译。

而:

protected abstract void convert();

会直接告诉编译器:

具体非抽象子类必须解决这个步骤。

因此 abstract 不只是减少代码。

它还能:

通过类型系统强制实现者完成必要步骤。


五、实践应用

5.1 数据导入

read
validate
convert
save

非常典型。

固定:

read
validate
save

变化:

convert

适合 Template Method。


5.2 文件处理

打开文件
↓
读取数据
↓
解析数据
↓
输出处理结果
↓
关闭文件

如果:

解析方式

因文件格式不同而变化,也可以考虑模板方法。

不过真实资源管理还涉及 IO 与异常,后面学习后再做完整版本。


5.3 报表生成

统一:

读取数据
↓
计算统计
↓
生成报表主体
↓
保存文件

不同报表:

日报
周报
月报

可能只改变:

统计算法
报表主体

同样适合分析固定与变化。


5.4 自动化测试流程

例如概念上:

准备测试环境
↓
执行测试
↓
收集结果
↓
清理环境

不同测试用例只实现:

执行测试

这也是类似的模板思想。


六、常见问题

6.1 什么是模板方法设计模式?

在父类中定义固定算法流程,把变化步骤交给子类实现的一种设计模式。


6.2 模板方法主要解决什么问题?

当前课程重点:

  • 重复流程代码;
  • 多个子类重复控制同一流程;
  • 固定流程容易被写乱;
  • 可变步骤与固定步骤职责混在一起。

6.3 模板方法通常放在哪里?

通常放:

父类 / 抽象父类

由父类控制整体流程。


6.4 什么是固定步骤?

所有实现都遵循相同逻辑的步骤。

可以由父类直接提供实现。


6.5 什么是可变步骤?

流程中必须存在,但不同子类实现方式不同的步骤。

可以设计为:

abstract

方法。


6.6 子类主要负责什么?

主要实现:

差异化步骤。

而不是重复控制整个公共流程。


6.7 模板方法为什么建议 final?

当流程不允许改变时:

final

可以阻止子类重写模板方法,从而保护算法骨架。


6.8 abstract step 为什么不直接写空方法?

因为:

abstract

能够强制具体子类提供实现。

空方法则可能让子类忘记实现却仍然正常编译。


6.9 模板方法一定只有一个抽象方法吗?

不一定。

可以根据变化点存在多个抽象步骤。


6.10 Template Method 只是为了少写代码吗?

不是。

它还把:

流程控制权

集中到父类,并限制子类只改变允许变化的部分。


6.11 模板方法和继承有什么关系?

模板父类通过继承让子类获得固定流程,同时让子类重写变化步骤。


6.12 模板方法和多态有什么关系?

父类模板内部调用抽象步骤时,运行时会根据实际子类对象执行相应重写实现。


6.13 所有代码重复都应该用 Template Method 吗?

不是。

只有存在:

稳定流程骨架
+
可变步骤
+
合理继承关系

时才值得考虑。


6.14 模板方法必须写 abstract class 吗?

本课程采用:

抽象类 + 抽象步骤

这一经典结构。

它非常适合当前案例。

后续学习接口、组合等技术后,还会看到其他组织变化行为的方法。


七、练习与验收

7.1 知识问答

  1. 什么是模板方法设计模式?
  2. 它主要解决什么问题?
  3. 什么叫固定流程?
  4. 什么叫可变步骤?
  5. 为什么模板方法通常写在父类?
  6. 为什么父类适合控制算法骨架?
  7. 为什么变化步骤适合设计成抽象方法?
  8. 子类在 Template Method 中主要承担什么职责?
  9. 为什么建议使用 final 修饰模板方法?
  10. 如果子类重写模板方法可能产生什么问题?
  11. Template Method 和抽象类是什么关系?
  12. Template Method 和方法重写是什么关系?
  13. Template Method 和多态是什么关系?
  14. 模板方法内部为什么能够执行子类重写实现?
  15. 模板方法一定只有一个 abstract step 吗?
  16. fixed step 可以设计成 private 吗?
  17. variable step 为什么可能使用 protected?
  18. 模板方法只是为了减少代码量吗?
  19. 什么情况下普通方法抽取已经足够?
  20. 为什么不能为了“使用设计模式”而强行创建继承层次?

7.2 代码阅读

禁止运行:

abstract class Processor {

    public final void process() {

        step1();

        step2();

        step3();
    }

    private void step1() {
        System.out.println("step1");
    }

    protected abstract void step2();

    private void step3() {
        System.out.println("step3");
    }
}

class AProcessor extends Processor {

    @Override
    protected void step2() {
        System.out.println("A step2");
    }
}

class BProcessor extends Processor {

    @Override
    protected void step2() {
        System.out.println("B step2");
    }
}

回答:

  1. 哪个方法是模板方法?
  2. 哪些步骤固定?
  3. 哪个步骤变化?
  4. 为什么 step2() 是 abstract?
  5. 为什么 process() 使用 final?
  6. 创建 AProcessor 后调用 process(),执行哪些方法?
  7. 创建 BProcessor 后有什么不同?

继续分析:

Processor processor =
        new AProcessor();

processor.process();

回答:

  1. processor 的编译时类型是什么?
  2. 实际对象是什么?
  3. process() 定义在哪个类?
  4. 运行到 step2() 时为什么执行 AProcessor 的实现?
  5. 这里使用了什么多态机制?

7.3 手写代码

任务一:DataImporter

按照:

read
validate
convert
save

设计:

DataImporter
├── CsvImporter
└── ExcelImporter

要求:

  • 整体流程由模板方法统一控制;
  • 模板方法使用 final;
  • read()validate()save() 使用父类固定实现;
  • convert() 为变化步骤;
  • CsvImporter、ExcelImporter 分别实现不同转换。

任务二:ReportGenerator

流程:

加载数据
↓
统计数据
↓
生成正文
↓
输出报表

设计:

DailyReportGenerator
WeeklyReportGenerator

要求自行判断:

  • 哪些步骤固定;
  • 哪些步骤变化;
  • 哪些方法 private;
  • 哪些方法 protected abstract;
  • 哪个方法 final。

任务三:文章发布流程

流程:

检查标题
↓
处理正文
↓
生成摘要
↓
发布

不同文章类型:

TutorialArticle
BlogArticle

其中:

处理正文

方式不同。

使用 Template Method 设计。

7.4 Debug

下面代码的问题是什么?

abstract class DataImporter {

    public void importData() {
        read();
        convert();
        save();
    }

    protected abstract void convert();

    private void read() {
    }

    private void save() {
    }
}

class CsvImporter extends DataImporter {

    @Override
    public void importData() {
        saveCsvDirectly();
    }

    @Override
    protected void convert() {
    }

    private void saveCsvDirectly() {
    }
}

已知业务要求:

所有导入器必须严格:
read → convert → save

回答:

  1. 当前模板方法最大设计漏洞是什么?
  2. CsvImporter 为什么能够绕开固定流程?
  3. 应该使用什么关键字保护模板方法?
  4. 修复代码。

下面代码:

abstract class DataImporter {

    public final void importData() {
        convert();
    }

    protected void convert() {
    }
}

已知:

每一种 importer 都必须提供自己的转换规则。

回答:

  1. 当前子类是否可以忘记重写 convert?
  2. 为什么?
  3. convert 更适合声明成什么?
  4. 修改后对子类产生什么编译期约束?

7.5 综合训练

设计一个:

DataImporter

模板系统。

固定整体流程:

1. read
2. validate
3. convert
4. save

业务要求:

CSVImporter:
CSV 转换规则

ExcelImporter:
Excel 转换规则

JsonImporter:
JSON 转换规则

要求:

  1. 设计抽象父类。
  2. 模板方法命名为:
importData()
  1. importData() 必须保护流程不被子类改写。
  2. 公共固定步骤放父类。
  3. 可变步骤使用 abstract。
  4. 三个子类只实现变化逻辑。
  5. 使用:
DataImporter importer

分别接收三个子类。 8. 调用同一个:

importer.importData();
  1. 画出其中一次完整方法调用链。

最后回答:

Template Method 到底把“控制权”放在父类还是子类?子类真正拥有的变化权又在哪里?

7.6 本章验收

关闭资料,确认自己能够:

  • [ ] 解释 Template Method 解决的问题。
  • [ ] 从重复代码中识别公共流程。
  • [ ] 区分固定步骤与可变步骤。
  • [ ] 把固定流程写成模板方法。
  • [ ] 使用 abstract 声明变化步骤。
  • [ ] 让不同子类只实现变化步骤。
  • [ ] 使用 final 保护固定流程。
  • [ ] 解释父类模板为什么可以调用子类实现。
  • [ ] 把多态、重写、抽象类与模板方法连起来。
  • [ ] 从零完成 DataImporter。
  • [ ] 判断什么时候没必要使用模板方法。
  • [ ] 解释“模式不是为了炫技,而是为了适配真实重复问题”。

如果你只会背:

模板方法
+
抽象方法
+
final

却不能从一段真实重复代码中找出:

哪些东西应该固定?
哪些东西应该变化?
谁负责控制整体流程?

那么 Template Method 还没有真正掌握。