我们之前封装了对象创建,方法调用,复杂接口,鸭子,这一次,我们要封装算法块。
咖啡与茶
在设计中,我们常常会遇到一些方法通用,另外一些方法却不一样的情况。例如现在咖啡店中,茶和咖啡的冲法非常相似。我们看看。
步骤 | 咖啡冲泡法 | 茶冲泡法 |
---|---|---|
1 | 水煮沸 boilWater() | 水煮沸 boilWater() |
2 | 沸水冲泡咖啡 brewCoffeeGrinds() | 沸水浸泡茶叶 steepTeaBag() |
3 | 咖啡入杯 pourInCup() | 茶入杯 pourInCup() |
4 | 加糖和牛奶 addSugarMilk() | 加柠檬 addLemon() |
首先,我们可以发现茶与咖啡非常相似,可以通过继承一个超类咖啡饮品CaffeinBeverage
去节省重复代码。例如第一步。
其次,这些步骤很多都类似。我们可以把这些步骤封装起来,成为一个步骤prepareRecipe()
。然后在超类中,实现相同的步骤,而将不同的步骤作为抽象方法,留给子类去实现。
具体来讲,我们实现了一个超类咖啡饮品CaffeinBeverage
,它的制作过程如下
- 水煮沸
- 冲泡
- 饮料入杯
- 加调料
那么,让我们来实现咖啡饮品CaffeinBeverage
吧
public abstract class CaffeinBeverage{
// 准备饮品的步骤,煮水boilWater,冲泡brew,入杯pourIncup,加佐料addCondiments,
// 其中煮水和入杯都一致,为具体方法。其他方法为抽象方法,交由子类实现
// 注意这里使用了final关键字
final void prepareRecipe(){
boilWater();
brew();
pourIncup();
addCondiments();
}
abstract void brew();
abstract void addCondiments();
void boilWater() {
System.out.println("Boiling water");
}
void pourIncup(){
System.out.println("Pouring into cup");
}
}
有了超类,我们来实现咖啡和茶两个子类
咖啡Coffee实现
public class Coffee extends CaffeineBeverage{
public void brew() {
System.out.println("Dripping Coffee through filter");
}
public void addCondiments() {
System.out.println("Adding Sugar and Milk");
}
}
茶的实现
public class Tea extends CaffeineBeverage {
public void brew() {
System.out.println("Steeping the tea");
}
public void addCondiments() {
System.out.println("Adding Lemon");
}
}
模板方法模式
在上述部分中,CaffeinBeverage
中的prepareRecipe()
实际上就是模板方法,作为一个算法的模板,某些方法由CaffeinBeverage
自己实现,某些由它的子类实现。
那么这么做有什么好处呢?我们可以对比一下用不用模板方法的区别
不用模板方法 | 用模板方法 |
---|---|
Coffee和Tea有重复代码 | CaffeinBeverage可以让代码复用最大化 |
算法改变则Coffee和Tea都要改变 | 算法只存在于父类,容易修改 |
算法的描述和实现分散在很多地方 | CaffeinBeverage专注算法描述,子类负责实现 |
新的步骤类似的饮品加入会很麻烦 | 模板方法提供了框架,新的饮品只需要实现自己的方法就好 |
了解了这些,下面有请模板方法模式登场。
模板方法模式在一个方法中定义了一个算法骨架,而将一些步骤延迟到子类。模板方法可以使得子类不改变算法结构的情况下,重新定义算法中某些步骤。
以下是它的类图
|
这是模板方法类模式的具体实现框架
|
钩子
然而实际上,算法的实现可能会和模板方法不一致,有的步骤可能会被执行,有的不会。这时我们就需要用到钩子。它可以让子类有能力对算法的不同点挂钩。
例如我们可以给咖啡饮品类上个钩子,让其子类自行决定是否要加调料
public abstract class CaffeineBeverageWithHook {
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
// 我们在这里加了一条判断函数,子类可以覆盖判断函数。从而自己决定要不要加调料
if (customerWantsCondiments()) {
addCondiments();
}
}
abstract void brew();
abstract void addCondiments();
void boilWater() {
System.out.println("Boiling water");
}
void pourInCup() {
System.out.println("Pouring into cup");
}
// 该方法就是一个钩子,子类可以覆盖它。
boolean customerWantsCondiments() {
return true;
}
}
我们可以实现带钩子的咖啡,让用户自己输入yes或no,判断要不要加调料
public class CoffeeWithHook extends CaffeineBeverageWithHook {
public void brew() {
System.out.println("Dripping Coffee through filter");
}
public void addCondiments() {
System.out.println("Adding Sugar and Milk");
}
public boolean customerWantsCondiments() {
String answer = getUserInput();
if (answer.toLowerCase().startsWith("y")) {
return true;
} else {
return false;
}
}
private String getUserInput() {
String answer = null;
System.out.print("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 ioe) {
System.err.println("IO error trying to read your answer");
}
if (answer == null) {
return "no";
}
return answer;
}
}
我们可以看出钩子可以让子类实现算法的可选部分
策略模式 VS 模板方法模式
策略模式和模板方法模式两者有些类似,它们都封装了算法,但是两者封装算法的目的和方法却截然不同。策略模式定义了算法家族,这些算法可以互换,用户可以使用这些不同的算法。而模板方法模式则是定义了一个算法的大纲,将某些算法由子类继承去实现。它们的不同点在于
- 模板方法模式对算法有着更高的控制,它控制了算法的结构,而策略模式对算法没有多少控制。
- 模板方法模式通过继承实现,而策略模式通过组合实现
- 模板方法模式中的子类依靠超类的方法实现,而策略模式则不需要依靠任何人