目录

模板方法模式

Template

引子

有些人没有咖啡就活不下去;有些人则离不开茶。两者共同的成分是什么?当然是咖啡因了! 但还不只这样。茶和咖啡的冲泡方式非常相似:

星巴巴咖啡冲泡法

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

星巴巴茶冲泡法

  1. 把水煮沸
  2. 用沸水冲泡茶叶
  3. 把茶倒进杯子
  4. 加柠檬
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 这是我们的咖啡类,用来煮咖啡
public class Coffee {
    // 这是我们的咖啡冲泡法
    void prepareRecipe() {
        boilWater();
        brewCoffeeGrinds();
        pourInCup();
        addSugerAndMilk();
    } 
    // 煮沸水
    private void boilWater() {
        System.out.println("Boiling water");
    } 
    // 冲泡咖啡
    private void brewCoffeeGrinds() {
        System.out.println("Dripping coffee through filter");
    } 
    // 把咖啡倒进杯子
    private void pourInCup() {
        System.out.println("Pouring into cup");
    } 
    // 加糖和奶
    private void addSugerAndMilk() {
        System.out.println("Adding Sugar and Milk");
    }
} 
// 这是我们的茶类,用来煮茶
public class Tea {
    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
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class CaffeineBeverage {
    // 现在,用同一个prepareRecipe()方法来处理茶和咖啡。
    // prepareRecipe()方法被声明为final,因为我们不希望子类覆盖这个方法
    // 我们将第2步和第4步泛化成为brew()和addCondiments()
    final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    } 
    // 因为咖啡和茶处理这些方法的做法不同,所以这两个方法必须被声明为抽象,
    // 剩余的东西留给子类去操心
    abstract void addCondiments();
    abstract void brew(); 
    public void boilWater() {
        System.out.println("Boiling water");
    } 
    public void pourInCup() {
        System.out.println("Pouring into cup");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class Coffee extends CaffeineBeverage {
    @Override
    public void brew() {
        System.out.println("Dripping coffee through filter");
    } 
    @Override
    public void addCondiments() {
        System.out.println("Adding Sugar and Milk");
    }
} 
public class Tea extends CaffeineBeverage {
    @Override
    public void brew() {
        System.out.println("Steeping the tea");
    } 
    @Override
    public void addCondiments() {
        System.out.println("Adding Lemon");
    }
}

认识模板方法

基本上,我们刚刚实现的就是模板方法模式。咖啡因饮料类的结构包含了实际的“模板方法”:prepareRecipe()方法。为什么? 因为: 1.毕竟它是一个方法。 2.它用作一个算法的模板,在这个例子中,算法是用来制作咖啡因饮料的。 在这个模板中,算法内的每一个步骤都被一个方法代表了。某些方法是由这个类(也就是超类)处理的,某些方法则是由子类处理的。需要由子类提供的方法,必须在超类中声明为抽象。

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

定义

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

模式是用来创建一个算法的模板。

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

抽象类是如何被定义的,它内含的模板方法和原语操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 这就是我们的抽象类。它被声明为抽象,用来作为基类,其子类必须实现其操作
public abstract class AbstractClass {
    // 这就是模板方法。它被声明为final,以免子类改变这个算法的顺序。
    final void templateMethod() {
        // 模板方法定义了一连串的步骤,每个步骤由一个方法代表
        primitiveOperation1();
        primitiveOperation2();
        concreteOperation();
    } 
    // 在这个范例中有两个原语操作,具体子类必须实现它们
    abstract void primitiveOperation1();
    abstract void primitiveOperation2(); 
    // 这个抽象类有一个具体的操作。
    void concreteOperation() {
        // ...
    }
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public abstract class AbstractClass {
    final void templateMethod() {
        primitiveOperation1();
        primitiveOperation2();
        concreteOperation();
        // 我们加进一个新方法调用
        hook();
    } 
    // 这两个方法还是和以前一样,定义成抽象,由具体的子类实现。
    abstract void primitiveOperation1();
    abstract void primitiveOperation2();
 
    // 这个具体的方法被定义在抽象类中。
    // 将它声明为final,这样一来子类就无法覆盖它。
    // 它可以被模板方法直接使用,或者被子类使用。
    final void concreteOperation() {
        // ...
    } 
    // 我们也可以有“默认不做事的方法”,我们称这种方法为“hook”(钩子)。
    // 子类可以视情况决定要不要覆盖它们。在下面,我们就会知道钩子的实际用途
    void hook() {}
}

对模板方法进行挂钩

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public abstract class CaffeineBeverageWithHook {
    final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        // 我们加上了一个小小的条件语句,而该条件是否成立,
        // 是由一个具体方法customerWantsCondiments()决定的。
        // 如果顾客“想要”调料,只有这时我们才调用addCondiments()。
        if (customerWantsCondiments()) {
            addCondiments();
        }
    } 
    abstract void addCondiments();
    abstract void brew();
 
    public void boilWater() {
        System.out.println("Boiling water");
    } 
    public void pourInCup() {
        System.out.println("Pouring into cup");
    } 
    // 我们在这里定义了一个方法,(通常)是空的缺省实现。这个方法只会返回true,不做别的事。
    // 这就是一个钩子,子类可以覆盖这个方法,但不见得一定要这么做。
    boolean customerWantsCondiments() {
        return true;
    }
}

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

好莱坞原则可以给我们一种防止“依赖腐败”的方法。当高层组件依赖低层组件,而低层组件又依赖高层组件,而高层组件又依赖边侧组件,而边侧组件又依赖低层组件时,依赖腐败就发生了。

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

CaffeineBeverage是我们的高层组件,它能够控制冲泡法的算法,只有在需要子类实现某个方法时,才调用子类。