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

本文介绍了模板方法模式,通过冲泡茶和咖啡的例子展示了如何封装算法并让子类实现特定步骤。模板方法定义了一组操作步骤,其中部分步骤是抽象的,由子类实现。文章还探讨了好莱坞原则和钩子方法,以及如何在Java中使用模板方法进行排序,如Arrays.sort()。此外,讨论了如何在实际编程中灵活应用模板方法模式。
摘要由CSDN通过智能技术生成

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

8.模板方法模式

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

8.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()在两个类中的方法完全一样,所以此处出现了重复代码
我们接下来的工作就是将共同的部分抽取出来,放在一个基类中。
在这里插入图片描述

但是值得我们注意的是两份冲泡方法都采用了相同的算法
在这里插入图片描述

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

8.2 抽象prepareRecipe()

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

在这里插入图片描述

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

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("添加糖和牛奶");
    }
}

我们在上面的操作中做了些什么,下面我用一张图就很直观的显示了我们做了什么
在这里插入图片描述

8.3 认识模板方法

其实我们刚刚实现的就是模板方法模式,让我们看看咖啡因饮料的结构
在这里插入图片描述

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

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("----------------------------");

    }
}

输出结果如下:
在这里插入图片描述

模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。
我们为什么要引入模板方法?
在这里插入图片描述

8.4 定义模板方法模式

在这里插入图片描述

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

/**
 * 这就是我们的抽象类,他被声明为抽象,用来作为基类
 * 其子类必须实现其操作
 */
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();
    }
}

在这里插入图片描述

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

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

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

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

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

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

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

8.7 好莱坞原则

在这里插入图片描述

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

好莱坞原则和模板方法

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

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

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

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

在这里插入图片描述

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]);
        }
    }
}

运行结果如下:
在这里插入图片描述

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

  • 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原则
封装变化
多用组合,少用继承
针对接口编程,不针对实现编程
为交互对象之问的松耦合设计而努力
类应该对扩展开放,对修改关闭
依赖抽象,不要依赖具体类
只和朋友交谈
别找我,我会找你
在这里插入图片描述
下一篇 《设计模式09—迭代器与组合模式》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZNineSun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值