Head First 设计模式总结 (八) 模板方法模式

本文基于《Head First 设计模式》一书,对模板方法模式进行了概括和总结。

模板方法模式

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

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

问题描述

泡茶和泡咖啡比较相似:
咖啡冲泡法:
(1)把水煮沸
(2)用沸水冲泡咖啡
(3)把咖啡倒进杯子
(4)加糖和牛奶
茶冲泡法:
(1)把水煮沸
(2)用沸水浸泡茶叶
(3)把茶倒进杯子
(4)加柠檬
由于泡茶和泡咖啡有一些相似的地方,因此,最开始写代码的时候可能会这样设计:
Head First 设计模式总结 (八) 模板方法模式
但是这样是不是还不够?这两者之间还有其他相似的地方!它们似乎都完成了这样的操作:
(1)把水煮沸
(2)用沸水泡咖啡或茶
(3)把饮料倒进杯子
(4)在饮料内加入适当的调料
因此我们可以编写一个饮料抽象类,将一些方法泛化:

abstract class CaffeineBeverage {
     final void prepareRecipe(){  //为了防止子类改变模板方法中的算法,可以将模板方法声明为final
         boilWater();
         brew();
         addCondiments();
         pourIntoCup();
     }
     /*
     * 由于咖啡和茶的浸泡方式和加入的调料不同,因此这两个方法是抽象的,具体实现放在咖啡和茶本身中实现
     * */
     abstract void brew(); // 酿造
     abstract void addCondiments();  //加调料

     void boilWater(){ //烧开水
        System.out.println("Boiling water");
     }
     void pourIntoCup(){  //将饮料倒入杯中
        System.out.println("pouring into cup");
     }
}

咖啡和茶的代码实现如下:

class Coffee extends CaffeineBeverage {
    @Override
    void brew() {
        System.out.println("Dripping coffee through filter");
    }
    @Override
    void addCondiments() {
        System.out.println("Adding sugar and milk");
    }
}

class Tea extends CaffeineBeverage {
    @Override
    void brew() {
        System.out.println("Steeping the tea");
    }
    @Override
    void addCondiments() {
        System.out.println("Adding Lemon");
    }
}

Head First 设计模式总结 (八) 模板方法模式

模板方法定义了一个算法的步骤,并允许子类为一个或者对各步骤提供实现。
上述代码中的一切都由抽象类CaffineBeverage决定,它拥有算法并且维护这个算法,对于子类来说,这个类的存在可以将代码的复用最大化。

对模板方法进行挂钩

"钩子"是一种声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在可以让子类有能力对算法的不同点进行挂钩。要不要挂钩,由子类自行决定。有了钩子,子类能决定要不要覆盖方法。如果子类不提供自己的方法,抽象类会提供一个默认的实现。
下面我们将之前的抽象类改写成带钩子的:

abstract class CaffeineBeverageWithHook {
    void prepareRecipe(){
        boilWater();
        brew();
        pourIntoCup();
        if (customerWantsCondiments()){ //当顾客想要调料时,才加入调料
            addCondiments();
        }
    }
    /*
     * 由于咖啡和茶的浸泡方式和加入的调料不同,因此这两个方法是抽象的,具体实现放在咖啡和茶本身中实现
     * */
    abstract void brew();
    abstract void addCondiments();

    void boilWater(){
        System.out.println("Boiling water");
    }
    void pourIntoCup(){
        System.out.println("pouring into cup");
    }
    boolean customerWantsCondiments(){  //这就是一个钩子,子类可以覆盖这个方法,但是不见得一定要这么做
        return true;
    }
}

要想使用"钩子",子类也必须做出相应的更改(以下是咖啡的代码):

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

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");
    }
//根据用户的输入确定是否需要放调料
    public boolean customWantsCondiments(){
        String answer = getUserInput();
        if (answer.toLowerCase().startsWith("y")){
            return true;
        }
        else
            return false;
    }
//该方法询问用户是否需要糖或者牛奶,通过命令行得到用户输入
    public 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 e) {
            System.err.println("IO error trying to read your answer");
        }
        if (answer == null)
            return "no";
        else
            return answer;
    }
}

[注] Java中的JFrame类的paint()方法也是一个钩子,在默认情况下,paint()方法是不做任何事的,但是如果我们覆盖这个方法,告诉它我们要画东西,就会按你的要求在Frame上画出想要的东西。

本文还涉及到一个新的设计原则——

好莱坞原则

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

在好莱坞原则下,我们允许低层组件将自己挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些底层组件。换句话说,高层组件对待底层组件的方式是“别调用我们,我们会调用你”。
Head First 设计模式总结 (八) 模板方法模式
Head First 设计模式总结 (八) 模板方法模式
好莱坞原则是用在创建框架或组件上的一种技巧,好让低层组件能够被挂钩进计算中,而且又不会让高层组件依赖低层组件。好莱坞原则教我们一个技巧,创建一个有弹性的设计,而又防止其他类太过依赖它们。

策略模式和模板方法模式都封装算法,一个用组合,一个用继承。
下面是模板方法模式和之前的策略模式和工厂方法模式的对比。

模式 叙述
模板方法 子类决定如何实现算法中的某些步骤
策略模式 封装可互换的行为,然后使用委托来决定要采用哪一个行为
工厂方法 由子类决定实例化哪个具体类