Design Pattern学习笔记之模板方法模式(the Template Method Pattern)

Design Pattern学习笔记之模板方法模式(the Template Method Pattern)

1.    引子--Whois?

在介绍过的设计模式中,我们一直在做封装的工作:封装对象的创建,封装方法调用,“封装”复杂接口,封装调用方与提供方接口的不兼容…

模板方法模式也是一种封装,它封装了包括好几个步骤的算法,子类可以按照实际需要实现算法中的特定步骤。

模板方法模式是对Hollywood准则(演员提交简历后,只能回家等: don’t call me, I will call you!)的应用。

2.    问题来了—星巴兹咖啡店的饮料制作流程

之前我们为星巴兹咖啡店做过费用计算相关的设计工作,现在我们关注别的问题,下面是星巴兹咖啡店中咖啡和茶不同的制作流程,如下图所示:

看看原来系统中实现的咖啡和茶:

public class Coffee {

 

    void prepareRecipe() {

       boilWater();

       brewCoffeeGrinds();

        pourInCup();

       addSugarAndMilk();

    }

 

    public void boilWater() {

       System.out.println("Boilingwater");

    }

 

    public void brewCoffeeGrinds() {

       System.out.println("DrippingCoffee through filter");

    }

 

    public void pourInCup() {

       System.out.println("Pouringinto cup");

    }

 

    public void addSugarAndMilk() {

       System.out.println("AddingSugar and Milk");

    }

}

public class Tea {

 

    void prepareRecipe() {

       boilWater();

       steepTeaBag();

       pourInCup();

       addLemon();

    }

 

    public void boilWater() {

       System.out.println("Boilingwater");

    }

 

    public void steepTeaBag() {

       System.out.println("Steepingthe tea");

    }

 

    public void addLemon() {

       System.out.println("AddingLemon");

    }

 

    public void pourInCup() {

       System.out.println("Pouringinto cup");

    }

}

Tea和Coffee两个类都有一个prepareRecipe的方法用于实现饮料的制作流程,然后分别有4个方法来实现饮料制作过程中需要完成的特定工作。有什么问题?有个明显的问题:存在重复代码,在两个类中boilWater和pourInCup几乎完全是一样的。

3.    第一次进化—消除重复代码

因为Tea和Coffee中boilWater方法和pourInCup的方法完全一样,很自然的做法就是引入父类,把这两个方法放到父类中实现,另外两个茶和咖啡不同的方法作为抽象类,由子类实现。类结构图如下:

引入父类CaffeineBeverage,boilWater方法和pourInCup方法放到父类中实现,Tea和Coffee继承自CaffeineBeverage。

4.    模式来了—换个角度看问题

其实制作咖啡和制作茶的过程中,看起来不同的第2步和第4步也有共通之处,我们换个说法来说明两种饮料的制作,你会看到换个说法后,原来不同之处被屏蔽掉了。

1.      Boil some water.

2.      Use the hot water to extractthe coffee or tea.

3.      Pour the resulting beverageinto a cup.

4.      Add the appropriate condimentsto the beverage.

基于以上的分析,我们把饮料制作的方法抽象为以下内容:

final void prepareRecipe() {

       boilWater();

       brew();

       pourInCup();

       addCondiments();

    }

         我们把prepareRecipe方法作为一般方法在父类中实现,由子类(Tea or Coffee)实现不同的brew方法和addCondiments方法。具体代码如下:

public abstract class CaffeineBeverage {

 

    final void prepareRecipe() {

       boilWater();

       brew();

       pourInCup();

       addCondiments();

    }

 

    abstract void brew();

 

    abstract void addCondiments();

 

    void boilWater() {

       System.out.println("Boilingwater");

    }

 

    void pourInCup() {

       System.out.println("Pouringinto cup");

    }

}

public class Coffee extends CaffeineBeverage {

    public void brew() {

       System.out.println("DrippingCoffee through filter");

    }

    public void addCondiments() {

       System.out.println("AddingSugar and Milk");

    }

}

public class Tea extends CaffeineBeverage {

    public void brew() {

       System.out.println("Steepingthe tea");

    }

    public void addCondiments() {

       System.out.println("AddingLemon");

    }

}

经过以上改造,CaffeineBeverage中的prepareRecipe方法就成为一个典型的模板方法,最终的效果也符合模板方法模式。为什么叫模板方法呢?

1.  显而易见prepareRecipe是一个方法。

2.  prepareRecipe定义了算法的模板,在本例中它定义了饮料的制作过程,boilWater、brew、pourInCup、addCondiments分别代表制作过程的一个特定操作。

应用模板方法后,程序实现和之前有什么差异?

1.  应用模板方法后,由模板方法控制算法,子类不可随意更改算法;之前由子类控制算法。

2.  应用模板方法后,最大程度实现代码共用;之前存在重复代码。

3.  应用模板方法前,如果修改算法,需要在多个子类中修改;应用之后,只需在父类中修改。

4.  应用模板方法前,如果新增一种饮料的制作,需要增加大量代码;应用之后,只需新增少量代码。

5.  应用模板方法前,制作饮料的过程扩散到所有子类中;应用之后,制作过程集中在父类中,子类只需实现特定的步骤。

5.    理论篇—模板方法模式的定义

The TemplateMethod Pattern defines the skeleton of an algorithm in a method, deferring somesteps to subclasses. Template Method lets subclasses redefine certain steps ofan algorithm without changing the algorithm’s structure.

模板方法模式在父类的方法中定义算法的结构,将算法中的一些步骤推迟到子类实现。模板方法模式允许子类在不改变算法结构的前提下重新定义算法的特定步骤。

模板方法模式的类图:

抽象父类中定义模板方法templateMethod,它由三次方法调用构成:primitiveOperation1, primitiveOperation2, concreteOperation;前两个方法为抽象方法,由子类实现;第三个方法在父类中实现。

6.    新问题—制作过程需要变化?

如果按照以上的设计改造星巴兹现有的信息系统,貌似很完美,但是如果有顾客过来点了一杯咖啡,但是他又不想加入糖和牛奶,你怎么办?

那就要引入新的hook的概念,要在经典的、完美的模板方法模式中挂上一个钩子,对于不同子类可能有不同的步骤,钩子提供一个默认实现,如果子类对于该步骤不敏感,可完全忽略钩子;如果有不同要求,提供符合自己要求的钩子实现即可。

看看代码:

public abstract class CaffeineBeverageWithHook {

 

    void prepareRecipe() {

       boilWater();

       brew();

       pourInCup();

       if (customerWantsCondiments()) {

           addCondiments();

       }

    }

 

    abstract void brew();

 

    abstract void addCondiments();

 

    void boilWater() {

       System.out.println("Boilingwater");

    }

 

    void pourInCup() {

       System.out.println("Pouringinto cup");

    }

 

    boolean customerWantsCondiments() {

       return true;

    }

}

public class CoffeeWithHook extends CaffeineBeverageWithHook {

 

    public void brew() {

       System.out.println("DrippingCoffee through filter");

    }

 

    public void addCondiments() {

       System.out.println("AddingSugar and Milk");

    }

 

    public boolean customerWantsCondiments() {

 

       String answer = getUserInput();

 

       if (answer.toLowerCase().startsWith("y")) {

           return true;

       } else {

           return false;

       }

    }

 

    private String getUserInput() {

       String answer = null;

 

       System.out.print("Would youlike milk and sugar with your coffee (y/n)? ");

 

       BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

       try {

           answer = in.readLine();

       } catch (IOException ioe) {

           System.err.println("IO errortrying to read your answer");

       }

       if (answer == null) {

           return "no";

       }

       return answer;

    }

}

上述钩子方法customerWantsCondiments提供了对饮料制作过程的一个分支选择,依照操作员的输入来确定是否添加糖和牛奶。

在以上的实现中,引入钩子方法customerWantsCondiments和将根据不同情况实现不同子类的addCondiments方法,有什么区别?

最大的区别在于,引入钩子方法后,制作过程的分支变化也完全在父类进行控制;如果使用不同子类来实现不同的情况(添加、不添加),则意味着将制作过程的变化完全放开给子类,父类不了解这种变化。

7.    不辨不明—没有傻问题

1.      在创建一个模板方法时,对于算法中的多个步骤,哪些步骤应该用抽象方法,哪些方法应该用钩子?

如果该步骤在每个子类中都必须实现,是算法中不可或缺的一部分,就用抽象方法;如果步骤是可选的,只有部分子类需要,用钩子,子类可根据需要实现自己的钩子方法,也可以使用父类中的默认实现。

2.      钩子方法一般用来做什么?

钩子方法有几个用途:第一个对于算法的可选部分,使用钩子方法,允许子类根据实际需求忽略该方法或者实现该方法;第二个用于重复执行算法中的特定步骤;第三个是我们在示例中看到的,对算法的特定步骤选择性执行。

3.      子类是否必须实现父类中的所有抽象方法?

是这样的。子类必须提供模板方法中所有步骤的完整实现,对于父类没有提供实现的抽象方法,必须在子类中提供实现。

4.      看起来在父类中定义的抽象方法不能太多,这样会给子类带来负担。

是这样的。在写模板方法时要考虑这个问题,抽象方法不能太多,这牵涉到方法的划分粒度,是一个权衡。另外,注意把不是所有子类必须实现的方法定义为钩子方法,这样可减轻子类的负担。

8.    理论篇—好莱坞准则

好莱坞准则说起来很简单:Don’tcall us, we’ll call you。对于面向对象的设计而言,这个准则意味着什么呢?在OO设计中应用该准则,主要是防止依赖关系的紊乱,如果系统中有从上层到底层的依赖,有底层到上层的依赖,有上层到其他上层,上层到旁路层次的依赖,底层又有对下层的依赖,那这个系统的依赖关系会很快腐烂,无法理解,不可维护。

应用好莱坞准则,我们把底层的模块像钩子一样挂到系统中,由上层模块决定什么时候使用底层模块,怎么使用底层模块;就像上层模块和底层模块签订了一个好莱坞协议:“你别调用我,由我在需要的时候调用你”。

在模板方法模式中,怎样体现了好莱坞准则?

由父类制订算法结构,并决定在什么时候调用子类。

9.    不辨不明—没有傻问题

1.      好莱坞准则和依赖反转准则的关系?

依赖反转准则指导我们尽量使用抽象类和接口,以实现松散耦合;好莱坞准则推荐设计让底层模块可以像钩子一样挂到系统中的框架,既实现上层模块和下层模块的交互,又实现两者的松散耦合;依赖反转准则是强准则,提供了具体的操作步骤。

2.      是否底层模块绝对不允许调用上层模块?

不是这样的。实际上在类的继承体系中,总是存在子类调用父类方法的情况(构造函数),我们要尽力避免的是由系统设计引起的明确的循环依赖。

10.             现实世界中的模板方法模式

模板方法模式是很常见的设计模式,在框架设计中大量使用到,在实际应用中,它们跟标准的模式定义有差异。我们来看看jdk中数组排序时对该模式的应用。

Array类中排序算法的实现(有简化):

public static void sort(Object[] a) {

        Object[] aux = (Object[])a.clone();

        mergeSort(aux, a, 0, a.length, 0);

}

public static void sort(Object[] a, int fromIndex, int toIndex) {

        rangeCheck(a.length, fromIndex,toIndex);

        Object[] aux = cloneSubarray(a,fromIndex, toIndex);

        mergeSort(aux, a, fromIndex,toIndex, -fromIndex);

}

private static void mergeSort(Object[] src,

                Object[] dest,

                int low,

                int high,

                int off) {

    int length = high - low;

 

    // Insertion sort on smallest arrays

        if (length < INSERTIONSORT_THRESHOLD) {

            for (int i=low; i<high; i++)

                for (int j=i; j>low &&

            ((Comparable)dest[j-1]).compareTo(dest[j])>0; j--)

                    swap(dest, j, j-1);

            return;

        }

}

在mergeSort中实现冒泡排序算法,如果想使用Array的排序算法,要求Array中的每一个元素必须实现Comparable接口。

我们实现一个鸭子的排序:

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 + " weighs" + weight;

    }

 

    public int compareTo(Object object) {

 

       Duck otherDuck = (Duck)object;

 

       if (this.weight < otherDuck.weight) {

           return -1;

       } else if (this.weight == otherDuck.weight) {

           return 0;

       } else { // this.weight> otherDuck.weight

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

       display(ducks);

 

       Arrays.sort(ducks);

 

       System.out.println("\nAftersorting:");

       display(ducks);

    }

 

    public static void display(Duck[] ducks) {

       for (int i = 0; i < ducks.length; i++) {

           System.out.println(ducks[i]);

       }

    }

}

11.             不辨不明—真的是模板方法模式?

1.      Array的排序真的是模板方法模式?

在模板方法模式的定义中,父类定义算法的结构,子类提供特定步骤的实现,Array的排序看起来可不是这样。在现实中应用模式时,需要考虑各种现实的约束,在Array方法的约束就是,要求排序算法对所有的数组都可用,而不是针对某一类型数组(子类),基于这样的约束,由具体对象实现comparable接口来实现具体的比较,由sort方法定义排序算法的结构。

2.      Array的排序算法更像策略模式?

策略模式中每个策略都提供算法的完整实现;而Array的sort方法不是个完整的排序实现,它依赖于Array元素实现的compareto方法来完成算法。

3.      JDK中应用模板方法模式的其他例子。

JFrame,Applets

12.             不辨不明—策略模式与模板方法模式

1.      策略模式定义一组接口相同的算法,客户端可以按照实际情况使用不同算法,而无需修改代码。

2.      模板方法模式使用继承技术,在父类中定义算法结构,由子类实现算法中的特定步骤;算法结构在父类中控制。

3.      策略模式使用组合技术,由子类实现算法的完整实现,提供运行期改变算法的能力。

4.      策略模式更灵活,但是比模板方法模式更复杂,不能提供算法结构的控制。

13.             Review

新模式:

The TemplateMethod Pattern defines the skeleton of an algorithm in a method, deferring somesteps to subclasses. Template Method lets subclasses redefine certain steps ofan algorithm without changing the algorithm’s structure.

模板方法模式在父类的方法中定义算法的结构,将算法包含的一些步骤推迟到子类实现。模板方法模式允许子类在不改变算法结构的前提下重新定义算法的特定步骤。

新准则:

Hollywood – Don’tcall us, we’ll call you. 别调用我,我会在需要的时候调用你。

模式回顾:

1.      模板方法定义算法结构,把算法的一些步骤推迟到子类中实现。

2.      模板方法模式提供了一种代码复用的技术,可实现最大程度的代码复用。

3.      父类中可提供算法步骤的具体方法,抽象方法以及钩子实现;抽象方法必须由每一个子类提供实现,钩子方法由父类提供默认实现,子类根据实际情况忽略或者覆盖。

4.      为了避免子类修改算法结构,可在父类中将模板方法设置为final。

5.      好莱坞准则指引我们做这样的设计:上层模块决定何时、如何调用底层模块。

6.      策略模式和模板方法模式都实现了对算法的封装。

7.      工厂模式是一种特殊的模板方法模式。

OO准则:

a. 封装变化,encapsulate what varies

b. 组合优于继承, favorcomposition over inheritance

c. 面向接口编程, programto interfaces, not implementation

d. 致力于实现交互对象之间的松散耦合, strive for loosely coupled designs between objects that interact

e. 类应该对于扩展开发,对于修改封闭, classes should be open for extension but closed for modification

f. 依赖于抽象类或者接口而不是具体类。Depend on abstraction. Do not depend on concrete classes.

g. 只跟关系最密切的对象打交道。Principle of Least Knowledge – talk only to your immediate friend.

h. 别调用我,由我在需要的时候调用你。Don’t call us, we’ll call you.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值