上一篇 《适配器模式与外观模式》
8.模板方法模式直到目前,我们的议题都绕着封装转;我们已经封装了对象创建、方法调用、复杂接口、鸭子、比萨……接下来呢我们将要深入封装算法块,好让子类可以在任何时候都可以将自己挂接进运算里。我们甚至会在本章学到一个受到好莱坞影响而启发的设计原则。
其主要作用就是用于将我们的算法封装起来
茶和咖啡的冲泡方式非常相似,大致如下:
接下来我们看一看冲泡咖啡的代码
public class Coffee {
void prepareRecipe() {
boilWater();
brewCoffeeGrinds();
pourInCup();
addSugarAndMilk();
}
public void boilWater() {
System.out.println("水沸腾了");
}
public void brewCoffeeGrinds() {
System.out.println("把咖啡加入沸水中");
}
public void pourInCup() {
System.out.println("把冲泡好的咖啡倒入杯子中");
}
public void addSugarAndMilk() {
System.out.println("加牛奶和糖");
}
}
煮茶类代码
public class Tea {
void prepareRecipe() {
boilWater();
steepTeaBag();
addLemon();
pourInCup();
}
public void boilWater() {
System.out.println("水沸腾了");
}
public void steepTeaBag() {
System.out.println("将茶叶加入沸水中");
}
public void addLemon() {
System.out.println("添加柠檬");
}
public void pourInCup() {
System.out.println("将茶水倒入杯子中");
}
}
注意:由于boilWater(),pourInCup()在两个类中的方法完全一样,所以此处出现了重复代码
我们接下来的工作就是将共同的部分抽取出来,放在一个基类中。
但是值得我们注意的是两份冲泡方法都采用了相同的算法
下面我们要想尽办法将prepareRecipe()方法抽象化。
8.2 抽象prepareRecipe() 1.我们所遇到的第一个问题,就是咖啡使用brewCoffeeGrinds()和addSugarAndMilk()方法,而茶使用steepTeaBag()和addLemon()方法。让我们来思考这一点:浸泡(steep)和冲泡(brew)差异其实不大。所以我们给它一个新的方法名称,比方说brew(),然后不管是泡茶或冲泡咖啡我们都用这个名称。类似地,加糖和牛奶也和加柠檬很相似:都是在饮料中加入调料。让我们也给它一个新的方法名称来解决这个问题,就叫做addCondiments()好了。这样一来,新的prepareRecipe()方法看起来就像这样:
public abstract class CaffeineBeverage {
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
abstract void brew();
abstract void addCondiments();
void boilWater() {
System.out.println("将水煮沸");
}
void pourInCup() {
System.out.println("将音频倒入杯中");
}
}
3.最后,我们需要处理咖啡和茶类了。这两个类现在都是依赖超类(咖啡因饮料)来处理冲泡法,所以只需要自行处理冲泡和添加调料部分:
Tea
public class Tea extends CaffeineBeverage {
@Override
public void brew() {
System.out.println("将茶叶放入沸水中");
}
@Override
public void addCondiments() {
System.out.println("添加柠檬");
}
}
Coffee
public class Coffee extends CaffeineBeverage {
@Override
public void brew() {
System.out.println("将咖啡粉放入沸水中");
}
@Override
public void addCondiments() {
System.out.println("添加糖和牛奶");
}
}
我们在上面的操作中做了些什么,下面我用一张图就很直观的显示了我们做了什么
其实我们刚刚实现的就是模板方法模式,让我们看看咖啡因饮料的结构
下面我们通过模板方法来冲泡茶和咖啡
public class BeverageTestDrive {
public static void main(String[] args) {
Tea tea = new Tea();
Coffee coffee = new Coffee();
System.out.println("开始制作茶");
System.out.println("----------------------------");
tea.prepareRecipe();
System.out.println("----------------------------");
System.out.println("开始制作咖啡");
System.out.println("----------------------------");
coffee.prepareRecipe();
System.out.println("----------------------------");
}
}
输出结果如下:
模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。
我们为什么要引入模板方法?
这个模式是用来创建一个算法的模板。什么是模板?
其实模板就是一个方法。更具体地说,这个方法将算法定义成一组步骤,其中的任何步骤都可以是抽象的,由子类负责实现。这可以确保算法的结构保持不变,同时由子类提供部分实现。
让我们细看抽象类是如何被定义的
public abstract class AbstractClass {
final void templateMethod() {
primitiveOperation1();
primitiveOperation2();
concreteOperation();
}
abstract void primitiveOperation1();
abstract void primitiveOperation2();
void concreteOperation() {
//这里是实现
}
}
接下来我们在靠近一点详细看看抽象类可以有哪些类型的方法
public abstract class AbstractClassHook {
final void templateMethod() {
primitiveOperation1();
primitiveOperation2();
concreteOperation();
hook();
}
abstract void primitiveOperation1();
abstract void primitiveOperation2();
void concreteOperation() {
//这里是实现
}
void hook() {
}
}
8.5 对模板方法进行挂钩
钩子是一种被声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在,可以让子类有能力对算法的不同点进行挂钩。要不要挂钩,由子类自行决定。
钩子有好几种用途,让我们先看其中一个,稍后再看其他几个:
public abstract class CaffeinBeverageWithHook {
void prepareRecipe() {
boilWater();
brew();
pourInCup();
if (customWantsCondiments()) {
addCondiments();
}
}
abstract void brew();
abstract void addCondiments();
void boilWater() {
System.out.println("把水煮沸");
}
void pourInCup() {
System.out.println("将饮料倒入杯子中");
}
boolean customWantsCondiments() {
return true;
}
}
8.6 使用钩子
为了测试钩子,我们在子类中覆盖它。钩子的作用是咖啡因饮料是否执行了某部分算法,说的更明确一些,就是饮料中是否要加进调料。
public class CoffeeWithHook extends CaffeinBeverageWithHook {
@Override
void brew() {
System.out.println("将咖啡粉放入沸水中");
}
@Override
void addCondiments() {
System.out.println("加入牛奶和糖");
}
@Override
public boolean customWantsCondiments() {
String answer = getUserInput();
if (answer.toLowerCase().startsWith("y")) {
return true;
} else {
return false;
}
}
private String getUserInput() {
String answer = null;
System.out.println("您需要咖啡里面加糖和牛奶吗?");
InputStream in;
BufferedReader inputt = new BufferedReader(new InputStreamReader(System.in));
try {
answer = inputt.readLine();
} catch (IOException ioe) {
System.out.println("IO error tring to read your answer");
}
if (answer == null) {
return "no";
}
return answer;
}
}
自己尝试一下模仿着CoffeeWithHook将TeaWithHook写出来
接下来我们开始进行测试
public class BeverageTestDriver {
public static void main(String[] args) {
TeaWithHook teaHook = new TeaWithHook();//创建一杯茶
CoffeeWithHook coffeeHook = new CoffeeWithHook();//创建一杯咖啡
System.out.println("开始制茶");
teaHook.prepareRecipe();
System.out.println("开始制作咖啡");
coffeeHook.prepareRecipe();
}
}
学到此处,我们可能会有些疑问
- 1.在使用模板方法时,怎么才能知道什么时候该使用抽象方法,什么时候使用钩子呢?
答:当你的子类“必须”提供算法中某个方法或步骤的实现时,就使用抽象方法。如果算法的这个部分是可选的,就用钩子。如果是钩子的话,子类可以选择实现这个钩子,但并不强制这么做。
- 2.子类必须实现抽象类中的所有方法吗?
答:是的,每一个具体的子类都必须定义所有的抽象方法,并为模板方法算法中未定义步骤提供完整的实现。
- 3.似平我应该保持抽象方法的数目越少越好,否则,在子类中实现这些方法将会很麻烦?
8.7 好莱坞原则答:当你在写模板方法的时候,心里要随时记得这一点。想要做到这一点,可以让算法内的步骤不要切割得太细,但是如果步骤太少的话,会比较没有弹性,所以要看情况折衷。也请记住,某些步骤是可选的,所以你可以将这些步骤实现成钩子,而不是实现成抽象方法这样就可以让抽象类的子类的负荷减轻。
好莱坞原则可以给我们一种防止“依赖腐败”的方法。当高层组件依赖低层组件,而低层组件又依赖高层组件,而高层组件又依赖边侧组件,而边侧组件又依赖低层组件时,依赖腐败就发生了。在这种情况下,没有人可以轻易地搞懂系统是如何设计的。
在好菜坞原则之下,我们允许低层组件将自己挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些低层组件。换句话说,高层组件对待低层组件的方式是“别调用我们,我们会调用你”
好菜坞原则和模板方法之间的连接其实还算明显:当我们设计模板方法模式时,我们告诉子类,“不要调用我们,我们会调用你”。怎样才能办到呢?让我们再看一次咖啡因饮料的设计:
- 1.好莱坞原则和依赖倒置原则之间的关系如何?
答:依赖倒置原则教我们尽量避免使用具体类,而多使用抽象。而好莱坞原则是用在创建框架或组件上的一种技巧,好让低层组件能够被挂钩进计算中,而且又不会让高层组件依赖低层组件。两者的目标都是在于解耦,但是依赖倒置原则更加注重如何在设计中避免依赖。
好莱坞原则教我们一个技巧,创建一个有弹性的设计,允许低层结构能够互相操作,而又防止其他类太过依赖它们。
- 2.低层组件不可以调用高层组件中的方法吗?
8.8 用模板方法排序答:并不尽然。事实上,低层组件在结束时,常常会调用从超类中继承来的方法。我们所要做的是,避免让高层和低层组件之间有明显的环状依赖。
Java数组类的设计者提供给我们一个方便的模板方法用来排序。让我们看看这个方法如何运行:
public class SortUtils {
public static void sort(Object[] a) {
Object aux[] = (Object[]) 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++) {
for (int j = i; j > low && ((Comparable) dest[j - 1]).compareTo((Comparable) dest[j]) > 0; j--) {
swap(dest, j, j - 1);
}
}
return;
}
private static void swap(Object[] src, int i, int j) {
Object temp = src[i];
src[i] = src[j];
src[j] = temp;
}
}
8.9 排序鸭子
假如我们有一个鸭子的数组需要排序,你要怎么做?
数组的排序模板方法已经提供了算法,但是你必须让这个模板方法知道如何比较鸭子。
而sort()的设计者希望这个方法能使用于所有的数组,所以他们把sort()变成是静态的方法,这样一来,任何数组都可以使用这个方法。但是没关系,它使用起来和它被定义在超类中是一样的。
现在,还有一个细节要告诉你:因为sort()并不是真正定义在超类中,所以sort()方法需要知道你已经实现了这个compareTo方法,否则就无法进行排序。
要达到这一点,设计者利用了Comparable接口。你须实现这个接口,提供这个接口所声明的方法,也就是compareTo()。
这个compareTo()方法将比较两个对象,然后返回其中一个是大于、等于还是小于另一个。sort()只要能够知道两个对象的大小,当然就可以进行排序。
比较鸭子好了,现在你知道了如果要排序鸭子,就必须实现这个compareTo()方法:然后,数组就可以被正常地排序了。
鸭子的实现如下:
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 + " 重量:" + weight;
}
@Override
public int compareTo(Object o) {
Duck otherDuck = (Duck) o;
if (this.weight < otherDuck.weight) {
return -1;
} else if (this.weight == otherDuck.weight) {
return 0;
} else {
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("未排序时的鸭子数组:");
display(ducks);
System.out.println("排序之后的鸭子数组:");
Arrays.sort(ducks);
display(ducks);
}
public static void display(Duck[] ducks) {
for (int i = 0; i < ducks.length; i++) {
System.out.println(ducks[i]);
}
}
}
运行结果如下:
- 1.首先我们需要一个鸭子数组
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)
};
- 2.然后调用Array类的sort()模板方法,并传入鸭子数组
Arrays.sort(ducks);
这个sort()方法控制排序过程
- 3.想要排序一个数组,你需要一次又一次地比较两个对象,直到整个数组都排序完毕。
当比较两只鸭子的时候,排序方法需要依赖鸭子的compareTo()方法,以得知谁大谁小。第一只鸭子的compareTo()方法被调用,并传入另一只鸭子当成比较对象:
ducks[0].compareTo(ducks[1])
- 4.如果鸭子的次序不对,就用Array的具体swap0方法将两者对调:
swap();
- 5.排序方法会持续比较并对调鸭子,直到整个数组的次序是正确的!
看到此处你可能会认为我是想象力太丰富,这些和模板方法有什么联系
这个模式的重点在于提供一个算法,并让子类实现某些步骤而数组的排序做法很明显地并非此如此
但是,我们都知道,荒野中的模式并非总是如同教科书例子一般地中规中矩。为了符合当前的环境和实理的约束,它们总是要被适当地修改。
这个Array 类sort()方法的设计者受到一些约束。
通常我们无法设计一个类继承java数组,而sort()方法希望能够适用于所有的数组(每个数组都是不同的类)。所以它们定义了一个静态方法,而由被排序的对象内的每个元素自行提供比较大小的算法部分。所以,这虽然不是教科书上的模板方法,但它的实现仍然符合模板方法模式的精神。再者,由于不需要继承数组就可以使用这个算法,这样使得排序变得更有弹性、更有用。
在Java API中还有其他模板方法的例子吗?
答:是的,你可以在一些地方看到它们。比方说,java.io的InputStream类有一个read()方法,是由子类实现的、而这个方法又会被read(byte b[], int off,int len)模板方法使用。
接下来我们继续丰富我们的OO原则
封装变化
多用组合,少用继承
针对接口编程,不针对实现编程
为交互对象之问的松耦合设计而努力
类应该对扩展开放,对修改关闭
依赖抽象,不要依赖具体类
只和朋友交谈
别找我,我会找你



