设计模式08—模板方法模式

上一篇 《适配器模式与外观模式》

8.模板方法模式

直到目前,我们的议题都绕着封装转;我们已经封装了对象创建、方法调用、复杂接口、鸭子、比萨……接下来呢我们将要深入封装算法块,好让子类可以在任何时候都可以将自己挂接进运算里。我们甚至会在本章学到一个受到好莱坞影响而启发的设计原则。
其主要作用就是用于将我们的算法封装起来

8.1 通过冲泡茶和冲泡咖啡来引入算法的封装

茶和咖啡的冲泡方式非常相似,大致如下:
设计模式08—模板方法模式_第1张图片

接下来我们看一看冲泡咖啡的代码

/**
 * 煮咖啡
 */
public class Coffee {
    void prepareRecipe() {
        boilWater();
        brewCoffeeGrinds();
        pourInCup();
        addSugarAndMilk();
    }

    public void boilWater() {
        System.out.println("水沸腾了");
    }

    public void brewCoffeeGrinds() {
        System.out.println("把咖啡加入沸水中");
    }

    public void pourInCup() {
        System.out.println("把冲泡好的咖啡倒入杯子中");
    }

    public void addSugarAndMilk() {
        System.out.println("加牛奶和糖");
    }
}

煮茶类代码

/**
 * 煮茶类
 */
public class Tea {
    void prepareRecipe() {
        boilWater();
        steepTeaBag();
        addLemon();
        pourInCup();
    }

    public void boilWater() {
        System.out.println("水沸腾了");
    }

    public void steepTeaBag() {
        System.out.println("将茶叶加入沸水中");
    }

    public void addLemon() {
        System.out.println("添加柠檬");
    }

    public void pourInCup() {
        System.out.println("将茶水倒入杯子中");
    }

}

注意:由于boilWater(),pourInCup()在两个类中的方法完全一样,所以此处出现了重复代码
我们接下来的工作就是将共同的部分抽取出来,放在一个基类中。
设计模式08—模板方法模式_第2张图片

但是值得我们注意的是两份冲泡方法都采用了相同的算法
设计模式08—模板方法模式_第3张图片

下面我们要想尽办法将prepareRecipe()方法抽象化。

8.2 抽象prepareRecipe()

1.我们所遇到的第一个问题,就是咖啡使用brewCoffeeGrinds()和addSugarAndMilk()方法,而茶使用steepTeaBag()和addLemon()方法。

设计模式08—模板方法模式_第4张图片

让我们来思考这一点:浸泡(steep)和冲泡(brew)差异其实不大。所以我们给它一个新的方法名称,比方说brew(),然后不管是泡茶或冲泡咖啡我们都用这个名称。类似地,加糖和牛奶也和加柠檬很相似:都是在饮料中加入调料。让我们也给它一个新的方法名称来解决这个问题,就叫做addCondiments()好了。这样一来,新的prepareRecipe()方法看起来就像这样:
设计模式08—模板方法模式_第5张图片

2.现在我们有了新的prepareRecipe()方法,但是需要让它能够符合代码。要想这么做,我们先从CaffeineBeverage(咖啡因饮料)超类开始:

/**
 * 咖啡因饮料是一个抽象类
 */
public abstract class CaffeineBeverage {
    /**
     * 现在.用同一个prepareRecipe()方法来处理茶和咖啡。piepareRecipe()被声明为final.
     * 因为我们不希望子类覆盖这个方法!我们将步骤2和步骤4泛化成为brew()和addCondiments()。
     */
    final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }

    /**
     * 为咖啡和茶处理这些方法的做法不同,所以这两个方法必须被声明为抽象,剩余余的东西留给子类去操心。
     */
    abstract void brew();

    abstract void addCondiments();

    void boilWater() {
        System.out.println("将水煮沸");
    }

    void pourInCup() {
        System.out.println("将音频倒入杯中");
    }
}

3.最后,我们需要处理咖啡和茶类了。这两个类现在都是依赖超类(咖啡因饮料)来处理冲泡法,所以只需要自行处理冲泡和添加调料部分:

Tea
/**
 * 茶和咖啡都是继承自咖啡因饮料
 */
public class Tea extends CaffeineBeverage {
    @Override
    public void brew() {
        System.out.println("将茶叶放入沸水中");
    }

    @Override
    public void addCondiments() {
        System.out.println("添加柠檬");
    }
}

Coffee
public class Coffee extends CaffeineBeverage {
    @Override
    public void brew() {
        System.out.println("将咖啡粉放入沸水中");
    }

    @Override
    public void addCondiments() {
        System.out.println("添加糖和牛奶");
    }
}

我们在上面的操作中做了些什么,下面我用一张图就很直观的显示了我们做了什么
设计模式08—模板方法模式_第6张图片

8.3 认识模板方法

其实我们刚刚实现的就是模板方法模式,让我们看看咖啡因饮料的结构
设计模式08—模板方法模式_第7张图片

下面我们通过模板方法来冲泡茶和咖啡

public class BeverageTestDrive {
    public static void main(String[] args) {
        Tea tea = new Tea();
        Coffee coffee = new Coffee();
        System.out.println("开始制作茶");
        System.out.println("----------------------------");
        tea.prepareRecipe();
        System.out.println("----------------------------");
        System.out.println("开始制作咖啡");
        System.out.println("----------------------------");
        coffee.prepareRecipe();
        System.out.println("----------------------------");

    }
}

输出结果如下:
设计模式08—模板方法模式_第8张图片

模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。
我们为什么要引入模板方法?
设计模式08—模板方法模式_第9张图片

8.4 定义模板方法模式

设计模式08—模板方法模式_第10张图片

这个模式是用来创建一个算法的模板。什么是模板?
其实模板就是一个方法。更具体地说,这个方法将算法定义成一组步骤,其中的任何步骤都可以是抽象的,由子类负责实现。这可以确保算法的结构保持不变,同时由子类提供部分实现。
让我们细看抽象类是如何被定义的

/**
 * 这就是我们的抽象类,他被声明为抽象,用来作为基类
 * 其子类必须实现其操作
 */
public abstract class AbstractClass {
    /**
     * 这就是模板方法。
     * 他被声明为final,以免子类改变这个算法的顺序
     */
    final void templateMethod() {
        /**模板方法定义了一连串的步骤,每一个步骤由一个方法代表。**/
        primitiveOperation1();
        primitiveOperation2();
        concreteOperation();
    }

    /**
     * 在这个范例中有两个原语操作,具体子类必须实现他们
     */
    abstract void primitiveOperation1();

    abstract void primitiveOperation2();

    void concreteOperation() {
        //这里是实现
    }
}

接下来我们在靠近一点详细看看抽象类可以有哪些类型的方法

/**
 * 我们加入了一个新方法调用,改变了templateMethod
 */
public abstract class AbstractClassHook {
    /**
     * 这就是模板方法。
     * 他被声明为final,以免子类改变这个算法的顺序
     */
    final void templateMethod() {
        /**模板方法定义了一连串的步骤,每一个步骤由一个方法代表。**/
        primitiveOperation1();
        primitiveOperation2();
        concreteOperation();
        hook();
    }

    /**
     * 在这个范例中有两个原语操作,具体子类必须实现他们
     */
    abstract void primitiveOperation1();

    abstract void primitiveOperation2();

    void concreteOperation() {
        //这里是实现
    }

    /**
     *这是一个具体的方法,但他什么都不做
     * 我们也可以有“默认不做事的方法”,我们称这种方法为“hook”(钩子)。
     * 子类可以视情况决定要不要覆盖它们。
     */
    void hook() {

    }
}

8.5 对模板方法进行挂钩

钩子是一种被声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在,可以让子类有能力对算法的不同点进行挂钩。要不要挂钩,由子类自行决定。
钩子有好几种用途,让我们先看其中一个,稍后再看其他几个:

public abstract class CaffeinBeverageWithHook {
    void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        /**
         * 我们加上了一个的条件语句,而该条件是否成立,是由一个具体方法
         * customWantsCondiments决定的。如果顾客“想要”调料,只有这时我们才调用
         * addCondiments
         */
        if (customWantsCondiments()) {
            addCondiments();
        }
    }

    abstract void brew();

    abstract void addCondiments();

    void boilWater() {
        System.out.println("把水煮沸");
    }

    void pourInCup() {
        System.out.println("将饮料倒入杯子中");
    }

    /**
     * 我们在这里定义了一个方法,通常是空的缺省实现。这个方法只会返回true,不做别的事
     * 这就是一个钩子,子类可以覆盖这个,但不见得一定要去覆盖
     *
     * @return
     */
    boolean customWantsCondiments() {
        return true;
    }
}

8.6 使用钩子

为了测试钩子,我们在子类中覆盖它。钩子的作用是咖啡因饮料是否执行了某部分算法,说的更明确一些,就是饮料中是否要加进调料。

public class CoffeeWithHook extends CaffeinBeverageWithHook {
    @Override
    void brew() {
        System.out.println("将咖啡粉放入沸水中");
    }

    @Override
    void addCondiments() {
        System.out.println("加入牛奶和糖");
    }

    /**
     * 让用户输入他们对调料的决定,根据用户的输入返回true或false
     *
     * @return
     */
    @Override
    public boolean customWantsCondiments() {
        String answer = getUserInput();
        if (answer.toLowerCase().startsWith("y")) {
            return true;
        } else {
            return false;
        }
    }

    private String getUserInput() {
        String answer = null;
        System.out.println("您需要咖啡里面加糖和牛奶吗?");
        InputStream in;
        BufferedReader inputt = new BufferedReader(new InputStreamReader(System.in));
        try {
            answer = inputt.readLine();
        } catch (IOException ioe) {
            System.out.println("IO error tring to read your answer");
        }
        if (answer == null) {
            return "no";
        }
        return answer;
    }
}

自己尝试一下模仿着CoffeeWithHook将TeaWithHook写出来
接下来我们开始进行测试

public class BeverageTestDriver {
    public static void main(String[] args) {
        TeaWithHook teaHook = new TeaWithHook();//创建一杯茶
        CoffeeWithHook coffeeHook = new CoffeeWithHook();//创建一杯咖啡
        System.out.println("开始制茶");
        teaHook.prepareRecipe();
        System.out.println("开始制作咖啡");
        coffeeHook.prepareRecipe();
    }
}

设计模式08—模板方法模式_第11张图片

学到此处,我们可能会有些疑问

  • 1.在使用模板方法时,怎么才能知道什么时候该使用抽象方法,什么时候使用钩子呢?

答:当你的子类“必须”提供算法中某个方法或步骤的实现时,就使用抽象方法。如果算法的这个部分是可选的,就用钩子。如果是钩子的话,子类可以选择实现这个钩子,但并不强制这么做。

  • 2.子类必须实现抽象类中的所有方法吗?

答:是的,每一个具体的子类都必须定义所有的抽象方法,并为模板方法算法中未定义步骤提供完整的实现。

  • 3.似平我应该保持抽象方法的数目越少越好,否则,在子类中实现这些方法将会很麻烦?

答:当你在写模板方法的时候,心里要随时记得这一点。想要做到这一点,可以让算法内的步骤不要切割得太细,但是如果步骤太少的话,会比较没有弹性,所以要看情况折衷。也请记住,某些步骤是可选的,所以你可以将这些步骤实现成钩子,而不是实现成抽象方法这样就可以让抽象类的子类的负荷减轻。

8.7 好莱坞原则

设计模式08—模板方法模式_第12张图片

好莱坞原则可以给我们一种防止“依赖腐败”的方法。当高层组件依赖低层组件,而低层组件又依赖高层组件,而高层组件又依赖边侧组件,而边侧组件又依赖低层组件时,依赖腐败就发生了。在这种情况下,没有人可以轻易地搞懂系统是如何设计的。
在好菜坞原则之下,我们允许低层组件将自己挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些低层组件。换句话说,高层组件对待低层组件的方式是“别调用我们,我们会调用你”
设计模式08—模板方法模式_第13张图片

好莱坞原则和模板方法

好菜坞原则和模板方法之间的连接其实还算明显:当我们设计模板方法模式时,我们告诉子类,“不要调用我们,我们会调用你”。怎样才能办到呢?让我们再看一次咖啡因饮料的设计:
设计模式08—模板方法模式_第14张图片

  • 1.好莱坞原则和依赖倒置原则之间的关系如何?

答:依赖倒置原则教我们尽量避免使用具体类,而多使用抽象。而好莱坞原则是用在创建框架或组件上的一种技巧,好让低层组件能够被挂钩进计算中,而且又不会让高层组件依赖低层组件。两者的目标都是在于解耦,但是依赖倒置原则更加注重如何在设计中避免依赖。
好莱坞原则教我们一个技巧,创建一个有弹性的设计,允许低层结构能够互相操作,而又防止其他类太过依赖它们。

  • 2.低层组件不可以调用高层组件中的方法吗?

答:并不尽然。事实上,低层组件在结束时,常常会调用从超类中继承来的方法。我们所要做的是,避免让高层和低层组件之间有明显的环状依赖。

设计模式08—模板方法模式_第15张图片

8.8 用模板方法排序

Java数组类的设计者提供给我们一个方便的模板方法用来排序。让我们看看这个方法如何运行:

public class SortUtils {
    /**
     * 第一个方法sort()只是一个辅助(helper)方法、用来创建一个数组的拷贝,然后将其传
     * 递给mergeSort()方法当作目标数组。同时传入mergeSort()的参数,还包括数组的长度、以及从
     * (0)开始排序。
     *
     * @param a
     */
    public static void sort(Object[] a) {
        Object aux[] = (Object[]) a.clone();
        mergeSort(aux, a, 0, a.length, 0);
    }

    /**
     * mergeeSort()方法包含排序算,此算法依赖于compareTo()方法实现
     *
     * @param src
     * @param dest
     * @param low
     * @param high
     * @param off
     */
    private static void mergeSort(Object[] src, Object dest[], int low, int high, int off) {//此处可以看成是一个模板方法
        for (int i = low; i < high; i++) {
            for (int j = i; j > low && ((Comparable) dest[j - 1]).compareTo((Comparable) dest[j]) > 0; j--) {
                swap(dest, j, j - 1);
            }
        }
        return;
    }

    /**
     * 交换i,j的值
     *
     * @param src
     * @param i
     * @param j
     */
    private static void swap(Object[] src, int i, int j) {
        Object temp = src[i];
        src[i] = src[j];
        src[j] = temp;
    }

}

8.9 排序鸭子

假如我们有一个鸭子的数组需要排序,你要怎么做?
数组的排序模板方法已经提供了算法,但是你必须让这个模板方法知道如何比较鸭子。
而sort()的设计者希望这个方法能使用于所有的数组,所以他们把sort()变成是静态的方法,这样一来,任何数组都可以使用这个方法。但是没关系,它使用起来和它被定义在超类中是一样的。
现在,还有一个细节要告诉你:因为sort()并不是真正定义在超类中,所以sort()方法需要知道你已经实现了这个compareTo方法,否则就无法进行排序。
要达到这一点,设计者利用了Comparable接口。你须实现这个接口,提供这个接口所声明的方法,也就是compareTo()。

什么是compareTo()?

这个compareTo()方法将比较两个对象,然后返回其中一个是大于、等于还是小于另一个。sort()只要能够知道两个对象的大小,当然就可以进行排序。

比较鸭子

好了,现在你知道了如果要排序鸭子,就必须实现这个compareTo()方法:然后,数组就可以被正常地排序了。
鸭子的实现如下:

/**
 * 我们之所以需要让鸭子类实现Compareable接口,因为我们无法真的让鸭子数组去继承数组。
 */
public class Duck implements Comparable {
    String name;
    int weight;

    public Duck(String name, int weight) {
        this.name = name;
        this.weight = weight;
    }

    public String toString() {
        return name + " 重量:" + weight;
    }

    /**
     * compareTo需要被传入另-鸭子。和本身这只鸭子做比较。
     * @param o
     * @return
     */
    @Override
    public int compareTo(Object o) {
        Duck otherDuck = (Duck) o;
        if (this.weight < otherDuck.weight) {
            return -1;
        } else if (this.weight == otherDuck.weight) {
            return 0;
        } else {
            return 1;
        }
    }
}

接下来我们进行一个测试

public class DuckSortTestDrive {
    public static void main(String[] args) {
        Duck[] ducks = {
                new Duck("Daffy", 8),
                new Duck("Dewey", 2),
                new Duck("Howard", 7),
                new Duck("Louie", 2),
                new Duck("Donald", 10),
                new Duck("Huey", 2)
        };
        System.out.println("未排序时的鸭子数组:");
        display(ducks);
        System.out.println("排序之后的鸭子数组:");
        Arrays.sort(ducks);
        display(ducks);
    }

    public static void display(Duck[] ducks) {
        for (int i = 0; i < ducks.length; i++) {
            System.out.println(ducks[i]);
        }
    }
}

运行结果如下:
设计模式08—模板方法模式_第16张图片

下面我们分析一下鸭子排序的内部是怎么实现的

  • 1.首先我们需要一个鸭子数组
     Duck[] ducks = {
                new Duck("Daffy", 8),
                new Duck("Dewey", 2),
                new Duck("Howard", 7),
                new Duck("Louie", 2),
                new Duck("Donald", 10),
                new Duck("Huey", 2)
        };
  • 2.然后调用Array类的sort()模板方法,并传入鸭子数组
Arrays.sort(ducks);

这个sort()方法控制排序过程

  • 3.想要排序一个数组,你需要一次又一次地比较两个对象,直到整个数组都排序完毕。
    当比较两只鸭子的时候,排序方法需要依赖鸭子的compareTo()方法,以得知谁大谁小。第一只鸭子的compareTo()方法被调用,并传入另一只鸭子当成比较对象:
ducks[0].compareTo(ducks[1])
  • 4.如果鸭子的次序不对,就用Array的具体swap0方法将两者对调:
swap();
  • 5.排序方法会持续比较并对调鸭子,直到整个数组的次序是正确的!

看到此处你可能会认为我是想象力太丰富,这些和模板方法有什么联系

这个模式的重点在于提供一个算法,并让子类实现某些步骤而数组的排序做法很明显地并非此如此
但是,我们都知道,荒野中的模式并非总是如同教科书例子一般地中规中矩。为了符合当前的环境和实理的约束,它们总是要被适当地修改。
这个Array 类sort()方法的设计者受到一些约束。
通常我们无法设计一个类继承java数组,而sort()方法希望能够适用于所有的数组(每个数组都是不同的类)。所以它们定义了一个静态方法,而由被排序的对象内的每个元素自行提供比较大小的算法部分。所以,这虽然不是教科书上的模板方法,但它的实现仍然符合模板方法模式的精神。再者,由于不需要继承数组就可以使用这个算法,这样使得排序变得更有弹性、更有用。

在Java API中还有其他模板方法的例子吗?

答:是的,你可以在一些地方看到它们。比方说,java.io的InputStream类有一个read()方法,是由子类实现的、而这个方法又会被read(byte b[], int off,int len)模板方法使用。

接下来我们继续丰富我们的OO原则
封装变化
多用组合,少用继承
针对接口编程,不针对实现编程
为交互对象之问的松耦合设计而努力
类应该对扩展开放,对修改关闭
依赖抽象,不要依赖具体类
只和朋友交谈
别找我,我会找你
设计模式08—模板方法模式_第17张图片
下一篇 《设计模式09—迭代器与组合模式》

你可能感兴趣的:(设计模式,模板方法模式,设计模式,design,mode,封装算法)