本章我们将要深入封装算法块,好让子类可以在任何时候都将自己挂接进运算里。
有些人不能没有咖啡;有些人则离不开茶,两者共同成分是什么?我们来看看茶和咖啡的冲泡方式:
星巴兹咖啡冲泡法:
1:把水煮沸
2:用沸水冲泡咖啡
3:把咖啡倒进杯子
4:加糖和牛奶
星巴兹茶冲泡法
1:把水煮沸
2:用沸水浸泡茶叶
3:把茶倒进杯子
4:加柠檬
快速搞定几个咖啡和茶的类
class Coffe{
void prepareRecipe(){
boilWater();
brewCoffeeGrinds();
pourInCup();
addSugarAndMilk();
}
puВLic void boilWater(){
System.out.println("Bolling water");
}
puВLic void brewCoffeeGrinds(){
System.out.println("Dripping Coffee through filter");
}
puВLic void pourInCup(){
System.out.println("Pouring into cup");
}
puВLic void addSugarAndMilk(){
System.out.println("Adding Sugar and Milk");
}
}
接下来是茶
class Tea{
void prepareRecipe(){
boilWater();
steepTeaBag();
pourInCup();
addLemon();
}
puВLic void boilWater(){
System.out.println("Bolling water");
}
puВLic void steepTeaBag(){
System.out.println("Steeping the tea");
}
puВLic void pourInCup(){
System.out.println("Pouring into cup");
}
puВLic void addLemon(){
System.out.println("Adding Lemon");
}
}
我们发现了重复的代码,似乎我们应该将共同的部分抽取出来,放进一个基类中。可能看起来像这样:
更近异步的设计
咖啡和茶还有什么其他共同点呢?
1:把水煮沸
2:用沸水跑咖啡或茶
3:把饮料倒进杯子
4:在饮料内加入适当的调料
1和3已经被我们抽圌出来,放到基类中了。
2和4这两个没有抽圌出来,但是他们是一样的,只是应用在不同的饮料上。
浸泡和冲泡差异其实不大。所以我们给它一个新的方法名称,brew(),然后不管泡茶或冲泡咖啡我们都用这个名称。类似的加糖和牛奶也和加柠檬很相似,都是在饮料中加入调料。让我们也给它一个新的方法名称,addCondiments()。这样一来新的prepareRecipe()方法就出来了。
我们先从CaffeineBeverage(咖圌啡圌因饮料)超类开始:
abstract class CaffeineBeverage{
final void prepareRecipe(){
boilWater();
brew();
pourInCup();
addCondiments();
}
puВLic void boilWater(){
System.out.println("Bolling water");
}
abstract void brew();
puВLic void pourInCup(){
System.out.println("Pouring into cup");
}
abstract void addCondiments();
}
最后我们需要处理咖啡和茶类了,这两个类现在都是依赖超类(咖圌啡圌因饮料)来处理冲泡法。所以只需要自行处理冲泡和添加调料部分:
class Tea_ extends CaffeineBeverage {
@Override
void brew() {
System.out.println("Steeping the tea");
}
@Override
void addCondiments() {
System.out.println("Adding Lemon");
}
}
class Coffe_ extends CaffeineBeverage{
@Override
void brew() {
System.out.println("Dripping Coffee through filter");
}
@Override
void addCondiments() {
System.out.println("Adding Sugar and Milk");
}
}
认识模板方法
基本上,我们刚刚实现的就是模板方法模式。在我们的咖啡饮料类(CaffeineBeverage)的结构,它包含了实际的“模板方法”。
prepareRecipe()就是我们的模板方法。它用作一个算法的模板,在这个例子中,算法是用来制作咖啡饮料的。
模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。
定义模板方法模式
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
//这就是我们的抽象类,它被声明为抽象,用来作为基类,其子类必须实现其操作
abstract class AbstractClass{
//这就是模板方法,它被声明为final,以免子类改变这个算法的顺序
final void templateMethod(){
primitiveOperation1();
primitiveOperation2();
concreteOperation();
}
//在这个范例中有两个原语操作,具体子类必须实现它们
abstract void primitiveOperation1();
abstract void primitiveOperation2();
void concreteOperation(){
//这个抽象类有一个具体的操作,关于这个方法,稍后会再详述
}
}
abstract class AbstractClass_{
//我们加进一个新方法调用,改变了templateMethod
final void templateMethod(){
primitiveOperation1();
primitiveOperation2();
concreteOperation();
hook();
}
abstract void primitiveOperation1();
abstract void primitiveOperation2();
final void concreteOperation(){
//这里是实现
//这个具体的方法被定义在抽象类中。将它声明为final,这样一来子类就无法覆盖它
//它可以被模板方法直接使用或被子类使用
}
/*
这是一个具体的方法,但是它什么事情都不做
我们也可以有“默认不做事的方法”,我们称这种方法为”hook“(钩子)
子类可以视情况决定要不要覆盖它们。后面会知道实际用途
*/
void hook(){}
}
对模板方法进行挂钩
钩子是一种被声明在抽象类中的方法,但只有空的或者默认的实现,钩子的存在可以让有能力对算法的不同点进行挂钩,要不要挂钩,由子类自行决定。
abstract class CaffeineBeverageWithHook{
final void prepareRecipe(){
boilWater();
brew();
pourInCup();
/*
我们加上了一个小小的条件语句,而该条件是否成立,是由一个具体方法
customerWantsCondiments()决定的,如果顾客想要调料,只有这时候
我们才调用addCondiments()。
*/
if(customerWantsCondiments()){
addCondiments();
}
}
puВLic void boilWater(){
System.out.println("Bolling water");
}
abstract void brew();
puВLic void pourInCup(){
System.out.println("Pouring into cup");
}
abstract void addCondiments();
/*
我们在这里定义了一个方法,(通常)是空的缺省实现,
这个方法只会返回true,不做别的事。
这就是一个钩子,子类可以负载这个方法,但不见得一定要这么做。
*/
boolean customerWantsCondiments(){
return true;
}
}
好莱坞原则:别调用我们,我们会调用你。
好莱坞原则可以给我们一种防止“依赖不好”的方法。当高层组件依赖底层组件,而底层组件又依赖高层组件,而刚曾组件又依赖边侧组件,而边侧组件又依赖底层组件时,依赖不好就发生了,这种情况,没人轻易搞懂系统是如何设计的。
在好莱坞原则之下,我们允许底层组件将自己挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些底层组件,换句话说,刚层组件对待底层组件的方式是“别调用我们,我们会调用你”。
荒野中的模板方法
模板方法模式是一个很常见的模式,到处都是,尽管如此,你必须用用一双锐利的眼睛,因为末班方法有旭东实现,而它们看起来并不一定和书上所讲的设计一直。
这个模式很常见是因为对创建框架来说,很棒,由框架控制如何做事情,而由你指定框架算法中每个步骤的细节。