白话设计模式之模板方法模式

91 阅读6分钟

熟练掌握设计模式是每个程序员的基本功,而在日常工作中运用好设计模式更能提高同事们的工作效率。良好的设计模式不仅可以降低学习成本,还能提升代码的可读性和可维护性。本专栏致力于以通俗易懂、逻辑清晰的方式讲解23种设计模式,帮助读者快速掌握各种设计模式并将其应用到实际项目中,提高编程水平。

目前已完成三篇:

本次带来的是模板方法模式。

算法思想

模板方法模式主要是将流程固定,每个操作的细节交由子类实现,并且允许子类更改流程,以提供更大的灵活性。

示例 —— 打工人的一天

最近几年总觉得时间过得很快,忙碌又机械的过完一天,对于打工人而言只有工作日和休息日,新的一天只是重复昨天的生活而已,没有一丝惊喜和憧憬。我上周五一想,这重复的生活不正好适用模板方法模式吗?所以我决定以此为素材写下这篇文章给自己创造几分欣喜😉。

上周四我的一天是:

吃早餐(吃豆浆加油条)、去公司(做地铁去公司)、开始工作开发新功能、打车回家

上周五我的一天是:

吃早餐(早上吃胡辣汤和小笼包)、时间仓促打车去公司、工作修昨天的bug、打车回家

一天的流程何其相似,无非就是吃早餐、去公司、工作、回家,模板方法的作用也是如此,可以把流程固定化,具体的操作可以交由子类自己实现。

类图

把类图画一下,大概就是抽象父类中定义吃早餐、去公司、工作、回家等四个抽象方法,子类去实现,父类中还需要定义一个“开启一天”的方法,并且在这个方法里固定流程顺序。

编码

  • 抽象基类
public abstract class AbstractWorkday {
    /**
     * 吃早餐
     */
    protected abstract void eatBreakfast();

    /**
     * 去公司
     */
    protected abstract void gotoCompany();

    /**
     * 工作一天
     */
    protected abstract void working();

    /**
     * 回家
     */
    protected abstract void goHome();

    /**
     * 开启我的一天
     */
    public final void startOneDay() {
        System.out.println("-------开启新的一天--------");
        eatBreakfast();
        gotoCompany();
        working();
        goHome();
    }
}
  • 具体实现类

为了避免代码重复,以下示例仅列举2023年3月31日的一天

public class Workday20230331 extends AbstractWorkday {

    @Override
    protected void eatBreakfast() {
        System.out.println("早上吃胡辣汤和小笼包");
    }

    @Override
    protected void gotoCompany() {
        System.out.println("打车去公司");
    }

    @Override
    protected void working() {
        System.out.println("开始工作,修bug");
    }

    @Override
    protected void goHome() {
        System.out.println("打车回家");
    }
}
  • 客户端调用
public class Client {
    public static void main(String[] args) {
        // 周四的一天
        AbstractWorkday oneDay = new Workday20230330();
        oneDay.startOneDay();
        // 周五的一天
        oneDay = new Workday20230331();
        oneDay.startOneDay();
    }
}
  • 打印结果

  • 结论

每次新增一天工作日的生活,只需新增一个实现了AbstractWorkday的类即可

解释

eatBreakfast、gotoCompany、working、goHome这些由抽象类定义,子类实现的方法属于基本方法

像startOneDay这种由若干个基本方法组成,并按照一定规律调用的方法称之为模板方法

模板方法模式就是这么简单。

钩子方法

从上面的代码中可以看出流程绝对固定,不具有灵活性,比如我不想吃早餐了该怎么办呢?可以使用钩子方法

钩子方法:钩子方法通常在父类中被定义为空方法或者默认方法,子类可以选择是否重写这些方法,并在其中添加自己的实现。父类可以直接调用钩子方法执行子类逻辑,也可根据钩子方法的返回值定向执行自身逻辑。

针对上述例子,可以增加一个isNeedEatBreakfast()方法并交由子类实现,父类在模板方法中根据isNeedEatBreakfast()的返回值做判断。

  • AbstractWorkday改动

子类如果不想有“吃早餐”的逻辑可以重写isNeedEatBreakfast

模板方法中根据isNeedEatBreakfast的返回值来判断是否执行eatBreakfast逻辑

public abstract class AbstractWorkday {
    /**
     * 是否吃早餐
     * 默认吃
     */
    protected boolean isNeedEatBreakfast() {
        return true;
    }

    /**
     * 开启我的一天
     */
    public final void startOneDay() {
        System.out.println("-------开启新的一天--------");
        if(isNeedEatBreakfast()) {
            eatBreakfast();
        }
        gotoCompany();
        working();
        goHome();
    }
}

注意事项

  • 模板方法一般写为final,防止后续团队成员继承更改执行过程。但是这就要求开发之初能够写出丰富的钩子方法能够满足后续业务的要求,否则可以考虑protected类型。也可以当业务复杂之后新增钩子方法,但是不太推荐,因为当前模板方法已经在线上运行了,此处的改动可能会影响到比较多的业务,不符合开闭原则。

优劣

优势

  • 将固定的流程统一抽离到父类中,钩子方法可以更改父类的执行流程,方便统一管理。
  • 将可变的部分交由子类继承重写,子类新增自定义逻辑不会干扰父类和其它子类,符合开闭原则。
  • 通过继承实现功能,代码结构更清晰,更有层次感。

劣势

  • 后续子类想定制流程时可能由于没有相应的钩子方法而新增,不符合开闭原则。
  • 钩子方法过多时代码结构会变得十分复杂,不具备易读性。

适用场景

  • 子类存在公共的流程,可以把这部分抽离并集中到父类中,作为模板方法,避免书写重复的代码。

Q&A

Q: 抽象基类的基本方法必须要是抽象的吗?

A: 如果大部分子类的基本方法逻辑相同,可以考虑统一放到父类中实现,访问权限修饰符改为protected支持重写。

设计Tips

这两周在阅读设计模式系列文章的时候有的新想法

  • 当出现书写重复代码的时候要考虑用设计模式改造一下。
  • 注意访问权限控制符,能用private就不用protected,保证访问权限尽量小,能用final就用final,要预防后续开发者因不熟悉逻辑出现不可预估的风险。

参考资料

  • 《设计模式之禅》第2版 —— 秦小波著
  • 大淘宝技术公众号设计模式系列

有不合理的地方,还请大佬们不吝赐教。文章中的代码放到了Github中。