上一篇 《适配器模式与外观模式》
直到目前,我们的议题都绕着封装转;我们已经封装了对象创建、方法调用、复杂接口、鸭子、比萨……接下来呢我们将要深入封装算法块,好让子类可以在任何时候都可以将自己挂接进运算里。我们甚至会在本章学到一个受到好莱坞影响而启发的设计原则。
其主要作用就是用于将我们的算法封装起来
接下来我们看一看冲泡咖啡的代码
/**
* 煮咖啡
*/
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()方法抽象化。
让我们来思考这一点:浸泡(steep)和冲泡(brew)差异其实不大。所以我们给它一个新的方法名称,比方说brew(),然后不管是泡茶或冲泡咖啡我们都用这个名称。类似地,加糖和牛奶也和加柠檬很相似:都是在饮料中加入调料。让我们也给它一个新的方法名称来解决这个问题,就叫做addCondiments()好了。这样一来,新的prepareRecipe()方法看起来就像这样:
/**
* 咖啡因饮料是一个抽象类
*/
public abstract class CaffeineBeverage {
/**
* 现在.用同一个prepareRecipe()方法来处理茶和咖啡。piepareRecipe()被声明为final.
* 因为我们不希望子类覆盖这个方法!我们将步骤2和步骤4泛化成为brew()和addCondiments()。
*/
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
/**
* 为咖啡和茶处理这些方法的做法不同,所以这两个方法必须被声明为抽象,剩余余的东西留给子类去操心。
*/
abstract void brew();
abstract void addCondiments();
void boilWater() {
System.out.println("将水煮沸");
}
void pourInCup() {
System.out.println("将音频倒入杯中");
}
}
/**
* 茶和咖啡都是继承自咖啡因饮料
*/
public class Tea extends CaffeineBeverage {
@Override
public void brew() {
System.out.println("将茶叶放入沸水中");
}
@Override
public void addCondiments() {
System.out.println("添加柠檬");
}
}
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,以免子类改变这个算法的顺序
*/
final void templateMethod() {
/**模板方法定义了一连串的步骤,每一个步骤由一个方法代表。**/
primitiveOperation1();
primitiveOperation2();
concreteOperation();
}
/**
* 在这个范例中有两个原语操作,具体子类必须实现他们
*/
abstract void primitiveOperation1();
abstract void primitiveOperation2();
void concreteOperation() {
//这里是实现
}
}
接下来我们在靠近一点详细看看抽象类可以有哪些类型的方法
/**
* 我们加入了一个新方法调用,改变了templateMethod
*/
public abstract class AbstractClassHook {
/**
* 这就是模板方法。
* 他被声明为final,以免子类改变这个算法的顺序
*/
final void templateMethod() {
/**模板方法定义了一连串的步骤,每一个步骤由一个方法代表。**/
primitiveOperation1();
primitiveOperation2();
concreteOperation();
hook();
}
/**
* 在这个范例中有两个原语操作,具体子类必须实现他们
*/
abstract void primitiveOperation1();
abstract void primitiveOperation2();
void concreteOperation() {
//这里是实现
}
/**
*这是一个具体的方法,但他什么都不做
* 我们也可以有“默认不做事的方法”,我们称这种方法为“hook”(钩子)。
* 子类可以视情况决定要不要覆盖它们。
*/
void hook() {
}
}
钩子是一种被声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在,可以让子类有能力对算法的不同点进行挂钩。要不要挂钩,由子类自行决定。
钩子有好几种用途,让我们先看其中一个,稍后再看其他几个:
public abstract class CaffeinBeverageWithHook {
void prepareRecipe() {
boilWater();
brew();
pourInCup();
/**
* 我们加上了一个的条件语句,而该条件是否成立,是由一个具体方法
* customWantsCondiments决定的。如果顾客“想要”调料,只有这时我们才调用
* addCondiments
*/
if (customWantsCondiments()) {
addCondiments();
}
}
abstract void brew();
abstract void addCondiments();
void boilWater() {
System.out.println("把水煮沸");
}
void pourInCup() {
System.out.println("将饮料倒入杯子中");
}
/**
* 我们在这里定义了一个方法,通常是空的缺省实现。这个方法只会返回true,不做别的事
* 这就是一个钩子,子类可以覆盖这个,但不见得一定要去覆盖
*
* @return
*/
boolean customWantsCondiments() {
return true;
}
}
为了测试钩子,我们在子类中覆盖它。钩子的作用是咖啡因饮料是否执行了某部分算法,说的更明确一些,就是饮料中是否要加进调料。
public class CoffeeWithHook extends CaffeinBeverageWithHook {
@Override
void brew() {
System.out.println("将咖啡粉放入沸水中");
}
@Override
void addCondiments() {
System.out.println("加入牛奶和糖");
}
/**
* 让用户输入他们对调料的决定,根据用户的输入返回true或false
*
* @return
*/
@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();
}
}
学到此处,我们可能会有些疑问
答:当你的子类“必须”提供算法中某个方法或步骤的实现时,就使用抽象方法。如果算法的这个部分是可选的,就用钩子。如果是钩子的话,子类可以选择实现这个钩子,但并不强制这么做。
答:是的,每一个具体的子类都必须定义所有的抽象方法,并为模板方法算法中未定义步骤提供完整的实现。
答:当你在写模板方法的时候,心里要随时记得这一点。想要做到这一点,可以让算法内的步骤不要切割得太细,但是如果步骤太少的话,会比较没有弹性,所以要看情况折衷。也请记住,某些步骤是可选的,所以你可以将这些步骤实现成钩子,而不是实现成抽象方法这样就可以让抽象类的子类的负荷减轻。
好莱坞原则可以给我们一种防止“依赖腐败”的方法。当高层组件依赖低层组件,而低层组件又依赖高层组件,而高层组件又依赖边侧组件,而边侧组件又依赖低层组件时,依赖腐败就发生了。在这种情况下,没有人可以轻易地搞懂系统是如何设计的。
在好菜坞原则之下,我们允许低层组件将自己挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些低层组件。换句话说,高层组件对待低层组件的方式是“别调用我们,我们会调用你”
好菜坞原则和模板方法之间的连接其实还算明显:当我们设计模板方法模式时,我们告诉子类,“不要调用我们,我们会调用你”。怎样才能办到呢?让我们再看一次咖啡因饮料的设计:
答:依赖倒置原则教我们尽量避免使用具体类,而多使用抽象。而好莱坞原则是用在创建框架或组件上的一种技巧,好让低层组件能够被挂钩进计算中,而且又不会让高层组件依赖低层组件。两者的目标都是在于解耦,但是依赖倒置原则更加注重如何在设计中避免依赖。
好莱坞原则教我们一个技巧,创建一个有弹性的设计,允许低层结构能够互相操作,而又防止其他类太过依赖它们。
答:并不尽然。事实上,低层组件在结束时,常常会调用从超类中继承来的方法。我们所要做的是,避免让高层和低层组件之间有明显的环状依赖。
Java数组类的设计者提供给我们一个方便的模板方法用来排序。让我们看看这个方法如何运行:
public class SortUtils {
/**
* 第一个方法sort()只是一个辅助(helper)方法、用来创建一个数组的拷贝,然后将其传
* 递给mergeSort()方法当作目标数组。同时传入mergeSort()的参数,还包括数组的长度、以及从
* (0)开始排序。
*
* @param a
*/
public static void sort(Object[] a) {
Object aux[] = (Object[]) a.clone();
mergeSort(aux, a, 0, a.length, 0);
}
/**
* mergeeSort()方法包含排序算,此算法依赖于compareTo()方法实现
*
* @param src
* @param dest
* @param low
* @param high
* @param off
*/
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;
}
/**
* 交换i,j的值
*
* @param src
* @param i
* @param j
*/
private static void swap(Object[] src, int i, int j) {
Object temp = src[i];
src[i] = src[j];
src[j] = temp;
}
}
假如我们有一个鸭子的数组需要排序,你要怎么做?
数组的排序模板方法已经提供了算法,但是你必须让这个模板方法知道如何比较鸭子。
而sort()的设计者希望这个方法能使用于所有的数组,所以他们把sort()变成是静态的方法,这样一来,任何数组都可以使用这个方法。但是没关系,它使用起来和它被定义在超类中是一样的。
现在,还有一个细节要告诉你:因为sort()并不是真正定义在超类中,所以sort()方法需要知道你已经实现了这个compareTo方法,否则就无法进行排序。
要达到这一点,设计者利用了Comparable接口。你须实现这个接口,提供这个接口所声明的方法,也就是compareTo()。
这个compareTo()方法将比较两个对象,然后返回其中一个是大于、等于还是小于另一个。sort()只要能够知道两个对象的大小,当然就可以进行排序。
好了,现在你知道了如果要排序鸭子,就必须实现这个compareTo()方法:然后,数组就可以被正常地排序了。
鸭子的实现如下:
/**
* 我们之所以需要让鸭子类实现Compareable接口,因为我们无法真的让鸭子数组去继承数组。
*/
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;
}
/**
* compareTo需要被传入另-鸭子。和本身这只鸭子做比较。
* @param o
* @return
*/
@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]);
}
}
}
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)
};
Arrays.sort(ducks);
这个sort()方法控制排序过程
ducks[0].compareTo(ducks[1])
swap();
看到此处你可能会认为我是想象力太丰富,这些和模板方法有什么联系
这个模式的重点在于提供一个算法,并让子类实现某些步骤而数组的排序做法很明显地并非此如此
但是,我们都知道,荒野中的模式并非总是如同教科书例子一般地中规中矩。为了符合当前的环境和实理的约束,它们总是要被适当地修改。
这个Array 类sort()方法的设计者受到一些约束。
通常我们无法设计一个类继承java数组,而sort()方法希望能够适用于所有的数组(每个数组都是不同的类)。所以它们定义了一个静态方法,而由被排序的对象内的每个元素自行提供比较大小的算法部分。所以,这虽然不是教科书上的模板方法,但它的实现仍然符合模板方法模式的精神。再者,由于不需要继承数组就可以使用这个算法,这样使得排序变得更有弹性、更有用。
在Java API中还有其他模板方法的例子吗?
答:是的,你可以在一些地方看到它们。比方说,java.io的InputStream类有一个read()方法,是由子类实现的、而这个方法又会被read(byte b[], int off,int len)模板方法使用。
接下来我们继续丰富我们的OO原则
封装变化
多用组合,少用继承
针对接口编程,不针对实现编程
为交互对象之问的松耦合设计而努力
类应该对扩展开放,对修改关闭
依赖抽象,不要依赖具体类
只和朋友交谈
别找我,我会找你
下一篇 《设计模式09—迭代器与组合模式》