Head First 设计模式总结 (八) 模板方法模式
本文基于《Head First 设计模式》一书,对模板方法模式进行了概括和总结。
模板方法模式
——在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
这个模式是用来创建一个算法的模板。什么是模板?模板就是一个方法。更具体地说,这个方法将算法定义成一组步骤,其中的任何步骤都可以是抽象的,由子类负责实现。这可以确保算法的结构保持不变,同时由子类提供部分实现。
问题描述
泡茶和泡咖啡比较相似:
咖啡冲泡法:
(1)把水煮沸
(2)用沸水冲泡咖啡
(3)把咖啡倒进杯子
(4)加糖和牛奶
茶冲泡法:
(1)把水煮沸
(2)用沸水浸泡茶叶
(3)把茶倒进杯子
(4)加柠檬
由于泡茶和泡咖啡有一些相似的地方,因此,最开始写代码的时候可能会这样设计:
但是这样是不是还不够?这两者之间还有其他相似的地方!它们似乎都完成了这样的操作:
(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");
}
}
模板方法定义了一个算法的步骤,并允许子类为一个或者对各步骤提供实现。
上述代码中的一切都由抽象类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上画出想要的东西。
本文还涉及到一个新的设计原则——
好莱坞原则
——别调用我们,我们会调用你。
好莱坞原则可以给我们一种防止“依赖腐败”的方法。当高层组件依赖低层组件,而低层组件又依赖高层组件,而高层组件又依赖边侧组件,而边侧组件又依赖低层组件时,依赖腐败就发生了。在这种情况下,没有人可以轻易地搞懂系统是如何设计的。
在好莱坞原则下,我们允许低层组件将自己挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些底层组件。换句话说,高层组件对待底层组件的方式是“别调用我们,我们会调用你”。
好莱坞原则是用在创建框架或组件上的一种技巧,好让低层组件能够被挂钩进计算中,而且又不会让高层组件依赖低层组件。好莱坞原则教我们一个技巧,创建一个有弹性的设计,而又防止其他类太过依赖它们。
策略模式和模板方法模式都封装算法,一个用组合,一个用继承。
下面是模板方法模式和之前的策略模式和工厂方法模式的对比。
模式 | 叙述 |
---|---|
模板方法 | 子类决定如何实现算法中的某些步骤 |
策略模式 | 封装可互换的行为,然后使用委托来决定要采用哪一个行为 |
工厂方法 | 由子类决定实例化哪个具体类 |