模板方法是什么

定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。TemplateMethod 使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。 摘自-《设计模式-可复用面向对象软件基础》

如果你之前不了解模板方法模式,那么直接理解这样一个定义稍显生硬。我们还是通过一个例子来简单说明下这个定义所描述的含义。

引入例子

我的身份证快要到期了,正好前两年在这个城市买了房,户口也一直没有迁移过来,所以前几天我就去了一趟公安局迁移了户口,并且用新的户口办了一张新的身份证。

往城市里迁移户口是需要具备一些条件的,比如说大学本科学历入户、投靠亲属入户、住房入户等等等等,只有满足了这些条件中的一种才具备往城市迁移户口的资格。按照我们多年面向对象的经验来说,我们会设计 学历入户类、投靠亲属类、住房入户类等。
我们知道公安局办理身份证需要一些流程,大致分为:准备材料,包括填写入户申请表等、材料审核、业务办理、原始户口迁出、迁入新户口这样一些流程。
好了,这下我们就给每个类创建具体的流程(也就是算法),包括上面所有的步骤。然后我们发现,有一些步骤实际上跟当前的类型并无直接的联系,也就是说这些步骤都是同样的代码在每个类中都会出现。因为每种类型都要经历差不太多的流程步骤,区别在于有些阶段准备的材料可能不一样。比如说,学历入户类型至少需要准备学历证书,投靠亲属类型需要准备亲属的户口簿、身份证,而住房入户则需要提供房产证等。除开这些地方不一样之外,像业务办理、原始户口迁出、迁入新户口这些步骤都是雷同的。
如何解决有些步骤的代码重复问题呢?

一个原则:封装不变部分,扩展可变部分。

我们可以把所有的步骤及先后顺序抽到一个超类中,在超类中定义好相同步骤的代码,对于有些步骤中不同的类型有不同的实现时,我们可以将其定义为抽象方法,让各个类型自己提供差异化的实现。这就是模板方法模式的核心思想。

理解模板方法的定义

有了上面的例子,我们再来尝试理解关于模板方法模式的定义。

  • 定义一个操作中的算法的骨架:一个操作可以类比为迁移户口,算法相当于整个迁移户口的工作流程,这这部分的主体针对的是超类,也就是指我们应封装那些统一的部分;
  • 将一些步骤延迟到子类中:比如上面的准备材料阶段,这部分的主体针对的是子类,也就是扩展可变的部分;
  • 使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤:算法的基本机构(有哪些步骤、先后顺序)是在超类中定义的,子类无需改变,子类关心的是整个算法中特定的步骤(准备材料)。

    模板方法类图分析

    经过上面的论述,应该对模板方法有了一个初步认识了。接下来,看一下模板方法的类图。
    行为型 - 模板方法模式(Template Method) - 图1
    模板方法模式是代码复用的一种极有效的手段,通过复用一些公共的代码,使得公用的部分变得更容易管理。一般,我们将定义算法的基本骨架的这个方法叫做模板方法,将抽象的方法称为原语操作。

事实上,在模板方法中还有一类更为出名的方法:钩子方法。钩子方法描述了这样的一种思想,超类中提供一个空的(或者默认的缺省行为)方法,当子类认为有必要替换这一行为的时候,可以重写这个钩子方法。
钩子方法和原语操作有一定的相似性,例如他们都提供了给子类扩展的手段,但他们并不能划等号。对于原语操作每个子类都应该提供各自的实现,而钩子方法则是根据需要决定是否应该重写

代码实现

例子对应的类图

AbstractDomicile.png
如上就是开篇提到的例子对应的类图,在超类中定义的

  • migrate() 方法就是模板方法,该方法中定义了整个流程的步骤(建议模板方法用 final 修饰,可以防止子类重写该方法);
  • prepareBasicMaterials() 方法为通用的普通方法,表示准备基本的材料;
  • additionalMaterials() 为原语操作,每个户口迁移方式需要提供不一样的附加材料;
  • doCheck() 为一个钩子方法,表示有些材料需要审查(比如学历入户方式,需要提供的学历证明不低于大学本科,而其他类型的入户方式则没有这个流程);
  • doBusiness() 和 grantCertificate() 方法均为通用的普通方法,分别表示办理户口迁移、发放新的户口簿。

    抽象类

    1. public abstract class AbstractDomicile {
    2. private final String username;
    3. public AbstractDomicile(String username) {
    4. this.username = username;
    5. }
    6. /**
    7. * 迁移户口
    8. */
    9. public final void migrate () {
    10. System.out.println(MessageFormat.format("||--> migrate domicile for {0} ------------------------------------|", this.username));
    11. this.prepareBasicMaterials();
    12. this.additionalMaterials();
    13. this.doCheck();
    14. this.doBusiness();
    15. this.grantCertificate();
    16. }
    17. /**
    18. * 准备基本材料
    19. */
    20. private void prepareBasicMaterials(){
    21. System.out.println(" 应准备好当前的身份证、原始户口簿、入户申请表");
    22. }
    23. /**
    24. * 准备附加材料
    25. */
    26. protected abstract void additionalMaterials();
    27. /**
    28. * 钩子方法,有些落户方式需要检查
    29. */
    30. protected void doCheck(){}
    31. /**
    32. * 办理业务
    33. */
    34. private void doBusiness(){
    35. System.out.println(" 提交资料,由工作人员审核及办理");
    36. System.out.println(" 户口已迁出");
    37. System.out.println(" 已迁入新户口");
    38. }
    39. /**
    40. * 拿证
    41. */
    42. private void grantCertificate(){
    43. System.out.println(" 发放新户口簿");
    44. }
    45. }

    具体实现类

    1. public class EducationEntryDomicile extends AbstractDomicile {
    2. public EducationEntryDomicile(String username) {
    3. super(username);
    4. }
    5. @Override
    6. protected void additionalMaterials() {
    7. System.out.println(" 还应准备好:学历证书、学位证书");
    8. }
    9. @Override
    10. protected void doCheck() {
    11. System.out.println(" 查验证书是否有效,并且学历至少要求为大学本科学历");
    12. }
    13. }
    1. public class HouseEntryDomicile extends AbstractDomicile {
    2. public HouseEntryDomicile(String username) {
    3. super(username);
    4. }
    5. @Override
    6. protected void additionalMaterials() {
    7. System.out.println(" 还应准备好:房产证书");
    8. }
    9. }
    1. public class RelativesEntryDomicile extends AbstractDomicile {
    2. public RelativesEntryDomicile(String username) {
    3. super(username);
    4. }
    5. @Override
    6. protected void additionalMaterials() {
    7. System.out.println(" 还应准备好:亲属的身份证、户口簿");
    8. }
    9. }

    测试代码

    1. public class Client {
    2. public static void main(String[] args) {
    3. AbstractDomicile domicile4Tom = new EducationEntryDomicile("Tom");
    4. domicile4Tom.migrate();
    5. AbstractDomicile domicile4Jack = new HouseEntryDomicile("Jack");
    6. domicile4Jack.migrate();
    7. AbstractDomicile domicile4Lisa = new RelativesEntryDomicile("Lisa");
    8. domicile4Lisa.migrate();
    9. }
    10. }
    1. ||--> migrate domicile for Tom ------------------------------------|
    2. 应准备好当前的身份证、原始户口簿、入户申请表
    3. 还应准备好:学历证书、学位证书
    4. 查验证书是否有效,并且学历至少要求为大学本科学历
    5. 提交资料,由工作人员审核及办理
    6. 户口已迁出
    7. 已迁入新户口
    8. 发放新户口簿
    9. ||--> migrate domicile for Jack ------------------------------------|
    10. 应准备好当前的身份证、原始户口簿、入户申请表
    11. 还应准备好:房产证书
    12. 提交资料,由工作人员审核及办理
    13. 户口已迁出
    14. 已迁入新户口
    15. 发放新户口簿
    16. ||--> migrate domicile for Lisa ------------------------------------|
    17. 应准备好当前的身份证、原始户口簿、入户申请表
    18. 还应准备好:亲属的身份证、户口簿
    19. 提交资料,由工作人员审核及办理
    20. 户口已迁出
    21. 已迁入新户口
    22. 发放新户口簿

    模板方法的扩展

    在源码经常都能见到模板方法的影子,这里举两个例子。

    在 jdk 中的应用

    1. public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
    2. // 省略其他代码
    3. public boolean add(E e) {
    4. add(size(), e);
    5. return true;
    6. }
    7. public void add(int index, E element) {
    8. throw new UnsupportedOperationException();
    9. }
    10. }

    在 add(E e) 中定义了添加元素调用的算法,而 add(int index, E element) 是一个应由子类实现的方法。

    在 Mybatis 中的应用

    ```java public abstract class BaseExecutor implements Executor { // 省略其他代码 public List flushStatements(boolean isRollBack) throws SQLException {

    1. if (this.closed) {
    2. throw new ExecutorException("Executor was closed.");
    3. } else {
    4. return this.doFlushStatements(isRollBack);
    5. }

    }

    protected abstract List doFlushStatements(boolean var1) throws SQLException;

} ``` 在 BaseExecutor 中,刷新语句的方法 flushStatements() 定义了算法,是模板方法;在该方法中,调用了抽象方法 doFlushStatements() ,让真正的刷新语句操作延迟到子类执行,是一个标准的模板方法模式的应用。