设计模式初探-行为型模式之模版方法模式

满堂花醉三千客,一剑霜寒十四州。在并发编程之AQS探秘中,我们有提到AbstractQueuedSynchroizer是基于模板方法模式设计的,那么到底什么是模板方法模式?运行的原理又是什么?下面就让我们带着这些问题一探究竟。本文包括以下部分:

  1. 前言
  2. 模板方法模式
    2.1 为何要用
    2.2 定义
    2.3 一个例子-奶茶
    2.4 添加钩子
    2.5 JDK中的模板方法
    2.6 模板方法VS策略模式VS工厂方法
  3. 总结

1. 前言

面向对象世界里的三大特性。封装、继承、多态盛名在外,无人不知无人不晓。设计模式往往也是围绕着这些特性展开的,就拿封装来说,工厂模式封装了对象的创建、命令模式封装了方法的调用、门面模式封装了复杂接口。接下来我们要封装算法


2. 模板方法模式

2.1 为什么要用

当我们用一种新东西的时候,着急去使用之前,不妨花一点时间想一想为什么要用,当我们想明白为什么要用后。才会让我们在接下来的运用中更加得心应手。

假设作者本人是个大厨😄,大厨现在要烹饪两个菜:红烧肉和糖醋排骨。
其中红烧肉的制作过程大致如下:

  1. 准备五花肉
  2. 倒入炒锅
  3. 翻炒
  4. 添加酱油
  5. 装盘

糖醋排骨的制作过程大致如下:

  1. 准备排骨
  2. 倒入炒锅
  3. 翻炒
  4. 添加糖
  5. 装盘
  • 第一版代码如下
package com.moxieliunian.template;
//红烧肉
public class BraisedPork {
    //食物制作
    void prepareFood(){
        //准备五花肉
        preparePork();
        //倒入炒锅
        putInPan();
        //翻炒
        fry();
        //添加酱油
        addSoy();
        //装盘出锅
        fill();
    }


    void preparePork(){
        System.out.println("准备红烧肉");
    }

    void putInPan(){
        System.out.println("倒入锅中");
    }

    void fry(){
        System.out.println("翻炒");
    }

    void addSoy(){
        System.out.println("添加酱油");
    }

    void fill(){
        System.out.println("装盘,制作完毕");
    }
}
package com.moxieliunian.template;
//糖醋排骨
public class Ribs {
    //食物制作
    void prepareFood(){
        //准备五花肉
        prepareRibs();
        //倒入炒锅
        putInPan();
        //翻炒
        fry();
        //添加糖
        addSugar();
        //装盘出锅
        fill();
    }


    void prepareRibs(){
        System.out.println("准备排骨");
    }

    void putInPan(){
        System.out.println("倒入锅中");
    }

    void fry(){
        System.out.println("翻炒");
    }

    void addSugar(){
        System.out.println("添加糖");
    }

    void fill(){
        System.out.println("装盘,制作完毕");
    }
}

我们发现了这两个类中大量的重复代码。既然有重复的代码,这表示我们可以重新整理下设计。那么如何做呢?很自然的想法就是把公共的代码提取出来,放到一个基类中。

  • 第二版代码如下
public abstract class FoodBase {
   abstract void prepareFood();
    void putInPan(){
        System.out.println("倒入锅中");
    }

    void fry(){
        System.out.println("翻炒");
    }

    void fill(){
        System.out.println("装盘,制作完毕");
    }
}
//红烧肉
public class BraisedPork extends FoodBase {
    @Override
    void prepareFood() {
        //准备五花肉
        preparePork();
        //倒入炒锅
        putInPan();
        //翻炒
        fry();
        //添加酱油
        addSoy();
        //装盘出锅
        fill();
    }

    private void preparePork() {
        System.out.println("准备红烧肉");
    }

    private void addSoy() {
        System.out.println("添加酱油");
    }
}
//糖醋排骨
public class Ribs extends FoodBase {
    @Override
        //食物制作
    void prepareFood() {
        //准备五花肉
        prepareRibs();
        //倒入炒锅
        putInPan();
        //翻炒
        fry();
        //添加糖
        addSugar();
        //装盘出锅
        fill();
    }


    private void prepareRibs() {
        System.out.println("准备排骨");
    }


    private void addSugar() {
        System.out.println("添加糖");
    }
}
红烧肉、糖醋排骨第二版类图.png

此时我们发现,算法的实现流程由子类控制,那么红烧肉和糖醋排骨还有什么共通点吗?
preparePork和prepareRibs,addSoy和addSugar 只是作用的对象不一样,但是具体的作用都是相同的。我们尝试做第三版抽象。

  • 第三版代码
public abstract class FoodBase {
    //规定了食物的制作流程
    protected void prepareFood() {
        //准备原材料
        prepareMaterial();
        //倒入炒锅
        putInPan();
        //翻炒
        fry();
        //添加配料
        addIngredient();
        //装盘出锅
        fill();
    }

    //准备原材料
    protected abstract void prepareMaterial();

    //添加配料
    protected abstract void addIngredient();

    private void putInPan() {
        System.out.println("倒入锅中");
    }

    private void fry() {
        System.out.println("翻炒");
    }

    private void fill() {
        System.out.println("装盘,制作完毕");
    }
}
//红烧肉
public class BraisedPork extends FoodBase {
    @Override
    protected void prepareMaterial() {
        System.out.println("准备红烧肉");
    }

    @Override
    protected void addIngredient() {
        System.out.println("添加酱油");
    }

}
//糖醋排骨
public class Ribs extends FoodBase {
    @Override
    protected void prepareMaterial() {
        System.out.println("准备排骨");
    }

    @Override
    protected void addIngredient() {
        System.out.println("添加糖");
    }
}

我们将preparePork和prepareRibs,addSoy和addSugar 抽象为prepareMaterial和addIngredient,此时类图如下:

红烧肉、糖醋排骨第三版类图.png

问:第三版代码(模板方法)相比前两版而言,有什么好处?
1. 算法的骨架由基类(FoodBase)规定且保护,且只存在基类中,后期修改只需要修改一处即可,便于维护。2. 子类只需要覆盖其需要覆盖的方法,可以达到最大化的代码复用。3. 同时可以时间算法的实现和算法本身想分离

2.2 定义

明白了上面的例子,我们就了解了模板方法的原理。模板方法在一个方法中定义了算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

模板方法类图.png

就拿我们上面的例子来说,模板方法FoodBase.prepareFood()规定了食物的制作流程为:准备原料、倒入炒锅、翻炒、添加配料、出锅。子类只需要实现准备原料和添加配料的方法即可。所有的子类食物制作方法都是按照这个流程进行。

2.3 一个例子-奶茶

现在有个奶茶店,生产两种奶茶:珍珠奶茶和红豆奶茶,其中珍珠奶茶制作流程如下:

  • 准备奶和茶
  • 倒入杯子
  • 添加珍珠
  • 加冰
  • 密封

而红豆奶茶制作如下

  • 准备奶和茶
  • 倒入杯子
  • 添加红豆
  • 加冰
  • 密封

这两个过程有着相同的算法骨架,我们很容易想到用模板方法模式来实现,如下:

//奶茶算法基类
public abstract class TeaBase {
    protected void prdouceTea(){
        //准备奶和茶
        prepareMilkAndTea();
        //倒入杯子
        putInCup();
        //添加配料
        addIngredient();
        //加冰
        addIce();
        //密封打包
        box();
    }

    private void prepareMilkAndTea(){
        System.out.println("准备奶和茶");
    }

    private void  putInCup(){
        System.out.println("倒入杯子");
    }

    protected abstract void addIngredient();

    private void addIce(){
        System.out.println("加冰");
    }

    private void box(){
        System.out.println("密封");
    }

}
//珍珠奶茶
public class BubbleTea extends TeaBase{
    @Override
    protected void addIngredient() {
        System.out.println("添加珍珠");
    }
}
//红豆奶茶
public class RedBeanTea extends TeaBase{
    @Override
    protected void addIngredient() {
        System.out.println("添加红豆");
    }
}

使用我们的算法

        BubbleTea bubbleTea=new BubbleTea();
        RedBeanTea redBeanTea=new RedBeanTea();
        System.out.println("珍珠奶茶制作开始");
        bubbleTea.prdouceTea();
        System.out.println("珍珠奶茶制作结束");
        System.out.println("红豆奶茶制作开始");
        redBeanTea.prdouceTea();
        System.out.println("红豆奶茶制作结束");
珍珠奶茶制作开始
准备奶和茶
倒入杯子
添加珍珠
加冰
密封
珍珠奶茶制作结束
红豆奶茶制作开始
准备奶和茶
倒入杯子
添加红豆
加冰
密封
红豆奶茶制作结束

可以看到算法按照我们规定的骨架,正确执行了

2.4 添加钩子

上面的例子中,我们使用模板方法模式来实现了奶茶的制作流程。但是有一个问题,无论是珍珠奶茶还是红豆奶茶,都默认加冰。如果要制作一杯不加冰的奶茶该如何操作呢?我们可以添加一个钩子,Hook。先看怎么用。

//奶茶算法基类
public abstract class TeaBase {
    protected void prdouceTea(){
        //准备奶和茶
        prepareMilkAndTea();
        //倒入杯子
        putInCup();
        //添加配料
        addIngredient();
        //默认加冰
        if (isNeedIce()){
            //加冰
            addIce();
        }
        //密封打包
        box();
    }

    private void prepareMilkAndTea(){
        System.out.println("准备奶和茶");
    }

    private void  putInCup(){
        System.out.println("倒入杯子");
    }

    protected abstract void addIngredient();
    //是否加冰,默认是
    protected boolean isNeedIce(){
        return true;
    }

    private void addIce(){
        System.out.println("加冰");
    }

    private void box(){
        System.out.println("密封");
    }

}
//由客户自己决定是否加冰的珍珠奶茶
public class SmartBubbleTea extends TeaBase{
    private boolean isNeedIce;
    SmartBubbleTea(boolean isNeedIce){
        super();
        this.isNeedIce=isNeedIce;
    }
    @Override
    protected void addIngredient() {
        System.out.println("添加珍珠");
    }

    @Override
    public boolean isNeedIce() {
        return isNeedIce;
    }
}

使用

        SmartBubbleTea smartBubbleTea = new SmartBubbleTea(false);
        System.out.println("不加冰珍珠奶茶制作开始");
        smartBubbleTea.prdouceTea();
        System.out.println("不加冰珍珠奶茶制作结束");
不加冰的珍珠奶茶制作开始
准备奶和茶
倒入杯子
添加珍珠
密封
不加冰的珍珠奶茶制作结束

可以看到,我们利用hook成功实现了,奶茶加不加冰的自由控制。

hook的作用有以下几点:

  • 钩子可以让子类实现算法的可选部分,或者在钩子对于子类的实现不重要的时候,子类可以对这个钩子置之不理
  • 钩子可以让子类对模板中即将发生的东西做出反应

2.5 JDK中的模板方法

上面说了那么多,都是我们自己在写demo。那么JDK中有没有用到模板方法的地方呢?当然是有的,比如:AbstractQueuedSynchronizer.acquire(int arg)、AbstractList.addAll(int index, Collection<? extends E> c)、Arrays.sort(Object[] a)等,以Arrays.sort为例,找出其中的模板方法

       public static void sort(Object[] a) {
        if (LegacyMergeSort.userRequested)
            legacyMergeSort(a);
        else
            ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
    }
    static void sort(Object[] a, int lo, int hi, Object[] work, int workBase, int workLen) {
        assert a != null && lo >= 0 && lo <= hi && hi <= a.length;

        int nRemaining  = hi - lo;
        if (nRemaining < 2)
            return;  // Arrays of size 0 and 1 are always sorted

        // If array is small, do a "mini-TimSort" with no merges
        if (nRemaining < MIN_MERGE) {
            int initRunLen = countRunAndMakeAscending(a, lo, hi);
            binarySort(a, lo, hi, lo + initRunLen);
            return;
        }
private static void binarySort(Object[] a, int lo, int hi, int start) {
        assert lo <= start && start <= hi;
        if (start == lo)
            start++;
        for ( ; start < hi; start++) {
            Comparable pivot = (Comparable) a[start];

            // Set left (and right) to the index where a[start] (pivot) belongs
            int left = lo;
            int right = start;
            assert left <= right;
            /*
             * Invariants:
             *   pivot >= all in [lo, left).
             *   pivot <  all in [right, start).
             */
            while (left < right) {
                int mid = (left + right) >>> 1;
                if (pivot.compareTo(a[mid]) < 0)
                    right = mid;
                else
                    left = mid + 1;
            }
            assert left == right;

可以看到,sort方法调用的时候,依赖于传入对象的compareTo方法,也就是说sort方法规定了排序的执行骨架,而由具体的类去决定排序的算法细节(重写compareTo)。这里之没有用到继承,一方面是因为数组不能被继承,另一方面则是因为排序方法希望能作用于所有的数组

我们可以看出设计模式往往并不是一尘不变的,使用中往往伴随着不同场景的适配。

2.6 模板方法VS策略模式VS工厂方法

模板方法模式和策略模式都是跟算法有关,那么这两个设计模式之间有什么异同呢?模板方法模式和工厂方法模式又有什么异同呢?

名称 定义 区别与联系
策略模式 定义了算法族,分别封装起来,让它们之间可以互换。 与模板方法一样都是作用于算法,不同的是策略模式侧重于算法的替换,使用组合来实现。
模板方法模式 在一个方法中定义了算法的骨架,将某些实现推迟到子类中,使子类可以改变实现细节,而不影响整体架构 与策略模式一样,都是作用于算法。不同的是模板方法侧重于子类改变算法的某个细节,而不会改变模板的整个算法骨架,使用继承实现。
工厂方法模式 定义了一个创建对象的接口,但由子类决定要实例化的是哪一个,将类的实例化推迟到子类中 工厂方法是模板方的一个特殊版本

3. 总结

本篇文章中,我们探讨了模板方法模式的相关内容。并和与之相似的设计模式:策略模式、工厂方法模式进行了简单对比。

由于技术水平所限,文章难免有不足之处,欢迎大家指出。我们下一篇文章见.....

参考文章
head first 设计模式

推荐阅读更多精彩内容