设计模式——8、模板方法模式
  • 作者:ZJWave
  • 分类: 设计模式
  • 发表:2019-06-13 09:51
  • 围观:6138
  • 评论:0

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

1.茶和咖啡的冲泡法

有些人没有咖啡就活不下去;有些人则离不开茶。两者共同的成分是什么?当然是咖啡因了!

但还不只是这样,茶和咖啡的冲泡方式非常相似,不信你瞧瞧:

星巴兹咖啡冲泡法:

  1. 把水煮沸
  2. 用沸水冲泡咖啡
  3. 把咖啡倒进杯子
  4. 加糖和牛奶

星巴兹茶冲泡法:

  1. 把水煮沸
  2. 用沸水浸泡茶叶
  3. 把茶倒进杯子
  4. 加柠檬

咖啡和茶的冲泡法大致上一样,不是吗?

快速搞定几个咖啡和茶的类(用Java语言)

让我们扮演“代码师傅”,写一些代码来创建咖啡和茶。下面是咖啡:

/**
 * 这是我们的咖啡类,用来煮咖啡
 */
public class Coffee {

    /**
     * 这是我们的咖啡冲泡法,
     * 直接取自训练手册。
     */
    void prepareRecipe(){
        boilWater();
        brewCoffeeGrinds();
        pourIncup();
        addSugarAndMilk();
    }

    public void boilWater() {
        System.out.println("Boiling water");
    }

    public void brewCoffeeGrinds() {
        System.out.println("Dripping Coffee through filter");
    }

    public void pourIncup() {
        System.out.println("Pouring into cup");
    }

    public void addSugarAndMilk() {
        System.out.println("Adding Sugar and Milk");
    }
}

接下来是茶:

public class Tea {

    /**
     * 这看起来和前面咖啡的实现很像,
     * 其中第2和第4个步骤不一样,但基本上是相同的冲泡法。
     */
    void prepareRecipe(){
        boilWater();
        steepTeaBag();
        pourIncup();
        addLemon();
    }
    
    private void boilWater() {
        System.out.println("Boiling water");
    }
    
    private void steepTeaBag() {
        System.out.println("Steeping the tea");
    }

    private void pourIncup() {
        System.out.println("Pouring into cup");
    }

    private void addLemon() {
        System.out.println("Adding Lemon");
    }

}

我们发现了重复的代码,这是好现象。这表示我们需要清理一下设计了。在这里,既然茶和咖啡是如此地相似,似乎我们应该将共同的部分抽取出来,放进一个基类中。

看起来这个咖啡和茶类的设计练习相当直接。你的第一版设计,可能看起来像这样:

更进一步的设计……

所以,咖啡和茶还有什么其他的共同点呢?让我们先从冲泡法下手。

注意两份冲泡法都采用了相同的算法:

  1. 把水煮沸。
  2. 用热水泡咖啡或茶。
  3. 把饮料倒进杯子。
  4. 在饮料内加入适当的调料。

那么,我们有办法将prepareRecipe()也抽象化吗?是的,现在就来看看怎么做

抽象prepareRecipe()

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

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

    void prepareRecipe(){
        boilWater();
        brew();
        pourIncup();
        addCondiments();
    }

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

/**
 * 咖啡因饮料是一个抽象类
 */
public abstract class CaffeineBeverage {

    /**
     * 现在,用同一个prepareRecipe()方法来处理茶和咖啡。
     * prepareRecipe()被声明为final,因为我们不希望子类覆盖这个方法。
     * 我们将步骤2和步骤4泛化称为brew()和addCondiments()
     */
    final void prepareRecipe(){
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }

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

    abstract void addCondiments();

    /**
     * 我们将这些移到咖啡因饮料类中
     */
    void pourInCup() {
        System.out.println("Pouring into cup");
    }

    void boilWater() {
        System.out.println("Boiling water");
    }
}

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

/**
 * 茶和咖啡都是继承自咖啡因饮料
 */
public class Tea extends CaffeineBeverage{

    /**
     * 查需要定义brew()和addCondiments(),
     * 这两个抽象方法来自Beverage类
     */
    @Override
    void brew() {
        System.out.println("Steeping the tea");
    }

    @Override
    void addCondiments() {
        System.out.println("Adding Lemon");
    }
}
public class Coffee extends CaffeineBeverage{

    /**
     * 咖啡也是一样,除了处理对象是咖啡,
     * 而调料是用糖和牛奶来取代茶包和柠檬
     */
    @Override
    void brew() {
        System.out.println("Dripping Coffee through filter");
    }

    @Override
    void addCondiments() {
        System.out.println("Adding Sugar and Milk");
    }
}

我们做了什么?

2.认识模板方法

基本上,我们刚刚实现的就是模板方法模式。这是什么?让我们看看咖啡因饮料类的结构,它包含了实际的“模板方法”:

模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。

让我们逐步地泡茶,追踪这个模板方法是如何工作的。你会得知在算法内的某些地方,该模板方法控制了算法。它让子类能够提供某些步骤的实现……

① 我们首先需要一个茶对象……

Tea myTea = new Tea();

② 然后我们调用这个模板方法:

myTea.prepareRecipe();

它会依照算法来只做咖啡因饮料……

③ 首先,把水煮沸:

boilWater();

这件事情是在咖啡因饮料类(超类)中进行的。

④ 接下来,我们需要泡茶,这件事情只有子类才知道要怎么做:

brew();

⑤ 现在把茶倒进杯子中,所有的饮料做法都一样,所以这件事情发生在超类中:

pourInCup();

⑥ 最后,我们加进调料,由于调料是各个饮料独有的,所以由子类来实现它:

addCondiments();

2.1 模板方法带给我们什么?

3.定义模板方法模式

你已经看到了在茶和咖啡的例子中如何使用模板方法模式。现在,就让我们来看看这个模式的正式定义和所有的细节:

模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

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

让我们看看类图:

让我们细看抽象类是如何被定义的,包括了它内含的模板方法和原语操作。

/**
 * 这就是我们的抽象类。它被声明为抽象,
 * 用来作为基类,其子类必须实现其操作
 */
abstract class AbstractClass {

    /**
     * 这就是模板方法。它被声明为final,
     * 以免子类改变这个算法的顺序。
     */
    final void templateMethod(){
        //模板方法定义了一连串的步骤,每个步骤由一个方法代表。
        primitiveOperation1();
        primitiveOperation2();
        concreteOperation();
    }

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

    abstract void primitiveOperation2();

    /**
     * 这个抽象类有一个具体的操作。
     */
    void concreteOperation() {
        //这里是实现
    }

}

现在我们要“更靠近一点”,详细看看此抽象类内可以有哪些类型的方法:

abstract class AbstractClass {

    final void templateMethod(){
        primitiveOperation1();
        primitiveOperation2();
        concreteOperation();
        //我们加进了一个新方法调用,改变了templateMethod
        hook();
    }

    //这两个方法还是和以前一样,定义成抽象,由具体的子类实现。
    abstract void primitiveOperation1();

    abstract void primitiveOperation2();

    /**
     * 这个具体的方法被定义在抽象类中。
     * 将它声明为final,这样一来子类就无法覆盖它。
     * 它可以被模板方法直接使用,或者被子类使用。
     */
    final void concreteOperation() {
        //这里是实现
    }

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

}

对模板方法进行挂钩……

钩子是一种被声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在,可以让子类有能力对算法的不同点进行挂钩。要不要挂钩,由子类自行决定。

钩子有好几种用途,让我们先看其中一个,稍后再看其他几个:

public abstract class CaffeineBeverageWithHook {

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

    abstract void brew();

    abstract void addCondiments();

    void pourInCup() {
        System.out.println("Pouring into cup");
    }

    void boilWater() {
        System.out.println("Boiling water");
    }

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

使用钩子

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

我们如何得知顾客是否想要调料呢?开口问不就行了!

public class CoffeeWithHook extends CaffeineBeverageWithHook {

    @Override
    void brew() {
        System.out.println("Dripping Coffee through filter");
    }

    @Override
    void addCondiments() {
        System.out.println("Adding Sugar and Milk");
    }

    /**
     * 你覆盖了这个钩子,
     * 提供了自己的功能。
     * @return
     */
    public boolean customerWantsCondiments(){
        //让用户输入他们对调料的决定。
        //根据用户的输入,返回true或false
        String answer = getUserInput();
        if(answer.toLowerCase().startsWith("y")){
            return true;
        }else {
            return false;
        }
    }

    /**
     * 这段代码询问用户是否想要奶和糖,通过命令行获得用户输入
     * @return
     */
    private String getUserInput() {
        String answer = null;
        System.out.println("Would you like 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 error trying to read your answer");
        }
        if(answer == null){
            return "no";
        }
        return answer;
    }
}

执行测试程序

下面是一段测试代码,用来制造热茶和热咖啡。

public class BeverageTestDrive {

    public static void main(String[] args) {
        TeaWithHook teaHook = new TeaWithHook();
        CoffeeWithHook coffeeHook = new CoffeeWithHook();

        System.out.println("\nMaking tea...");
        teaHook.prepareRecipe();

        System.out.println("\nMaking coffee...");
        coffeeHook.prepareRecipe();
    }
}

萌新:我反倒认为询问顾客的这类功能应该让所有的子类共用,不是吗?

你知道吗?我同意你的看法。但是你必须承认,这个例子实在很酷,钩子竟然能够作为条件控制,影响抽象类中的算法流程,实在很不赖吧!

我们相信,在你自己的代码中一定可以找到其他真实的场面可以使用模板模式和钩子。

3.1 Q&A

问:当我创建一个模板方法时,怎么才能知道什么时候该使用抽象方法,什么时候使用钩子呢?

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

问:使用钩子真正的目的是什么?

答:钩子有几种用法。如我们之前所说的,钩子可以让子类实现算法中可选的部分,或者在钩子对于子类的实现并不重要的时候,子类可以对此钩子置之不理。钩子的另一个用法,是让子类能够有机会对模板方法中某些即将发生的(或刚刚发生的)步骤做出反应。比方说,名为justReOrderedList()的钩子方法允许子类在内部列表重新组织后执行某些动作(例如在屏幕上重新显示数据)。正如你刚刚看到的,钩子也可以让子类有能力为其抽象类作一些决定。

问:子类必须实现抽象类中的所有方法吗?

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

问:似乎我应该保持抽象方法的数目越少越好,否则,在子类中实现这些方法将会很麻烦。

答:当你在写模板方法的时候,心里要随时记得这一点。想要做到这一点,可以让算法内的步骤不要切割的太细,但是如果步骤太少的话,会比较没有弹性,所以要看情况折衷。

也请记住,某些步骤是可选的,所以你可以讲这些步骤实现成钩子,而不是实现成抽象方法,这样就可以让抽象类的子类的负荷减轻。

4.好莱坞原则

我们有一个新的设计原则,称为好莱坞原则:

好莱坞原则:别调用(打电话给)我们,我们会调用(打电话给)你。

很容易记吧?但这和OO设计又有什么关系呢?

好莱坞原则可以给我们一种防止“依赖腐败”的方法。当高层组件依赖低层组件,而低层组件又依赖高层组件,而高层组件又依赖边侧组件,而边侧组件又依赖低层组件时,依赖腐败就发生了。在这种情况下,没有人可以轻易地搞懂系统是如何设计的。

在好莱坞原则之下,我们允许低层组件将自己挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些低层组件。换句话说,高层组件对待低层组件的方式是“别调用我们,我们会调用你”。

4.1 好莱坞原则和模板方法

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

4.2 Q&A

问:好莱坞原则和依赖倒置原则之间的关系如何?

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

问:低层组件不可以调用高层组件中的方法吗?

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

5.Java类库中的模板方法

模板方法模式是一个很常见的模式,到处都是。尽管如此,你必须拥有一双锐利的眼睛,因为模板方法有许多实现,而他们看起来并不一定和书上所讲的设计一致。

这个模式很常见是因为对创建框架来说,这个模式简直棒极了。有框架控制如何做事情,而有你(使用这个框架的人)指定框架算法中每个步骤的细节。

5.1 用模板方法排序

我们经常需要数组做什么事情?对了!排序。

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

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


    private static void mergeSort(Object[] src,Object[] dest,int low,int high,int off) {
        for(int i = low; i < high; i++) {
            //我们需要实现compareTo()方法,“填补”模板方法的
            for (int j = i; j > low && ((Comparable)dest[j-1]).compareTo((Comparable)dest[j])  > 0 ; j--) {
                //这是一个具体方法,已经在数组类中定义了
                swap(dest,j,j-1);
            }
        }
        return;
    }

加入我们有一个鸭子的数组需要排序,你要怎么做?数组的排序模板方法已经提供了算法,但是你必须让这个模板方法指导如何比较鸭子。你所要做的事情就是,实现一个compareTo()方法……听起来有道理吧?

萌新:不,听起来没有道理。我们不是应该要继承什么东西吗?我认为这才是模板方法的关键所在。数组无法继承,所以我不知道要如何使用sort()。

很好的观点!事情是这样的:sort()的设计者希望这个方法能使用于所有的数组,所以他们把sort()编程是静态的方法,这样一来,任何数组都可以使用这个方法。但是没关系,它使用起来和它被定义在超类中是一样的。现在,还有一个细节要告诉你:因为sort()并不是真正定义在超类中,所以sort()方法需要知道你已经实现了这个compareTo()方法,否则就无法进行排序。

要达到这一点,设计者利用了Comparable接口。你须实现这个接口,提供这个接口所声明的方法,也就是compareTo()。

什么是compareTo()?

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

比较鸭子

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

鸭子的实现如下:

/**
 * 我们需要让鸭子类实现Comparable接口,
 * 因为我们无法真的让鸭子数组去继承数组
 */
public class Duck implements Comparable<Duck> {
    //鸭子有名字和体重
    String name;
    int weight;

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

    @Override
    public String toString() {
        return "Duck{" +
                "name='" + name + '\'' +
                ", weight=" + weight +
                '}';
    }

    /**
     * compareTo()需要被传入另一只鸭子,和本身这只鸭子作比较
     *
     * @param o
     * @return
     */
    @Override
    public int compareTo(Duck o) {
        //我们在这里指定鸭子是如何比较的,
        //如果这只鸭子的体重比另一只鸭子体重轻,就返回-1
        //如果相等,就返回0,
        //如果这只鸭子的体重较大,就返回1。
        if(this.weight < o.weight){
            return -1;
        }else if(this.weight == o.weight){
            return 0;
        }else{//this.weight > o.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("Before sorting:");
        display(ducks);
        //开始排序了
        //请注意,我们调用Arrays类的静态方法sort(),然后将鸭子数组当做参数传入
        Arrays.sort(ducks);

        System.out.println("\nAfter sorting:");
        //再将它们打印出来,看鸭子们的名字和体重
        display(ducks);
    }

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

观察鸭子排序的内部工作

让我们追踪Array类的sort()模板方法的工作过程。我们会看到模板方法是如何控制算法的,以及在算法中的某些电商它是如何要求我们的鸭子提供某个步骤的实现的……

① 首先,我们需要一个鸭子数组:

Duck[] ducks = { new Duck("Daffy",8), ... };

② 然后调用Array类的sort()模板方法,并传入鸭子数组:

Arrays.sort(ducks);

这个sort()方法(和它的helper mergeSort())控制排序过程

③ 想要排序一个数组,你需要一次又一次地比较两个项目,知道整个数组都排序完毕。

当比较两只鸭子的时候,排序方法需要依赖鸭子的compareTo()方法,以得知谁大谁小。第一只鸭子的compareTo()方法被调用,并传入另一只鸭子当成比较对象:

ducks[0].compareTo(ducks[1]);

④ 如果鸭子的次序不对,就用Array的具体swap()方法将两者对调:

swap()

⑤ 排序方法会持续比较并对调鸭子,知道整个数组的次序是正确的!

Q&A

问:这真的是一个模板方法模式吗?还是你的想象力太丰富了?

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

问:排序的实现实际上看起来更像是策略模式,而不是模板方法模式。为什么我们要将它归为模板方法?

答:你之所以会这么认为,可能是因为策略模式使用对象组合。在某种程度上,你是对的——我们使用数组对象排序我们的数组,这部分和策略模式非常相似。但是请记住,在策略模式中,你所组合的类实现了整个算法。数组所实现的排序算法并不完整,它需要一个类填补compareTo()方法的实现。因此,我们认为这更像模板方法。

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

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

写一个Swing的窗口程序

也许你没用过JFrame,在这里简单解释一下。它是最基本的Swing容器,继承了一个paint()方法。在默认状态下,paint()是不做事情的,因为它是一个“钩子”。通过覆盖paint(),你可以将自己的代码插入JFrame的算法中,显示出你所想要的画面。下面是一个超级简单的例子:

/**
 * 我们扩展了JFrame,它包含一个update()方法,这个方法控制更新屏幕的算法
 * 我们可以通过覆盖paint()钩子方法和这个算法挂上钩
 */
public class MyFrame extends JFrame {

    public MyFrame(String title){
        //不用管里面的细节,这只是一些初始化的动作
        super(title);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setSize(300,300);
        this.setVisible(true);
    }

    /**
     * JFrame的更新算法被称为paint()。
     * 在默认状态下,paint()是不做事的……
     * 它只是一个钩子。我们覆盖paint(),
     * 告诉JFrame在窗口上面画出一条消息
     * @param g
     */
    @Override
    public void paint(Graphics g) {
        super.paint(g);
        String msg = "I rule!!";
        g.drawString(msg,100,100);
    }


    public static void main(String[] args) {
        new MyFrame("Hello");
    }

}

因为我们利用了paint()钩子方法,所以可以显示出这样的消息。

6.模板方法模式与策略模式对比

工厂方法模式:

  • 定义一个算法的大纲,由子类定义其中某些步骤的内容
  • 通过继承,在算法中的个别步骤可以有不同的实现细节,但算法的结构维持不变
  • 对算法有更多的控制权,并允许子类指定行为
  • 需要超类中提供基础方法,依赖程度较高。

策略模式

  • 定义一个算法家族,并让这些算法可以互换。
  • 通过对象组合的方式,让客户可以选择算法实现。
  • 采用委托模型,使用对象组合,可以在运行时改变使用的算法。
  • 更有弹性,依赖程度较低。

7.总结

有了模板方法,你就可以像专家一样复用代码,同时保持对算法的控制。

OO原则:

  • 封装变化
  • 多用组合,少用继承
  • 针对接口编程,不针对实现编程
  • 为交互对象之间的松耦合设计而努力
  • 类应该对扩展开放,对修改关闭。
  • 依赖抽象,不要依赖具体类。
  • 最少知识原则(只和朋友谈)
  • 别找我,我会找你。

我们最新的原则提醒你,由超类主控一切,当它们需要的时候,自然会去调用子类,这就跟好莱坞一样。

OO模式:

模板方法模式——在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

本文要点:

  • “模板方法”定义了算法的步骤,把这些步骤的实现延迟到子类。
  • 模板方法模式为我们提供了一种代码复用的重要技巧。
  • 模板方法的抽象类可以定义具体方法、抽象方法和钩子。
  • 抽象方法由子类实现。
  • 钩子是一种方法,它在抽象类中不做事,或者制作默认的事情,子类可以选择要不要去覆盖它。
  • 为了防止子类改变模板方法中的算法,可以将模板方法声明为final。
  • 好莱坞原则告诉我们,将决策权放在高层模块中,以便决定如何以及何时调用低层模块。
  • 你将在真实世界代码中看到模板方法模式的许多变体,不要期待它们全都是一眼就可以被你认出的。
  • 策略模式和模板方法模式都封装算法,一个用组合,一个用继承。
  • 工厂方法是模板方法的一种特殊版本。

转载请注明原文链接:ZJ-Wave

Top