一、了解模板方法模式
1.1 什么是模板方法模式
模板方法模式 Template Method Parrern)在一个方法中定义了一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义方法中的某些步骤。
模板就是一个方法,更具体的说,这个方法将算法定义为一组步骤,其中的任何步骤都可以是抽象的,由子类负责实现。这可以确保算法的结构保持不变,同时由子类提供部分实现。
1.2 模板方法模式组成结构
- 抽象类(AbstractClass):包含模板方法,定义了算法的骨架。专注于算法本身。
- 具体类(ConcreteClass):实现抽象类中的抽象方法,完成算法的实现。
1.3 模板方法 UML 图解
1.4 模板方法的适用场景
- 在某些类的算法中,用了相同的方法,造成代码的重复。
- 控制子类扩展,子类必须遵守算法规则。
二、模板方法模式具体应用
2.1 问题描述
茶与咖啡:有的人喜欢喝茶,而有的人 喜欢喝咖啡。茶与咖啡 之间有什么共同的成分吗?答案是咖啡因 (怎么样,没想到吧)。不止是这样,茶与咖啡的冲泡方式也十分类似。如下
茶冲泡法:
- 把水煮沸
- 用沸水冲泡咖啡
- 把咖啡倒进杯子
- 加糖和牛奶
咖啡冲泡法:
- 把水煮沸
- 用沸水浸泡茶叶
- 把茶倒进杯子
- 加柠檬
现在我们来设计代码,完成茶与咖啡的冲泡。
2.2 简单设计实现
因为代码比较简单,所以这里只提供代码实现的设计图。从上面的设计图中,我们应该能发现一个问题:存在着一些重复的代码 (boilWater() 与 pourInCup())。所以我们要重新考虑一下设计,来避免这个问题。
PS:有一个重要的设计原则:找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
于是我们可能想到使用继承,把不变的代码方法超类中,把变化的代码放在子类中去实现。
2.3 使用继承
这种通过继承的设计感觉还不错,可以很好的解决问题。但是我们仔细想想,冲泡茶与咖啡是不是还存在一些共同点呢?其实 brewCoffeeGrinds() 与 steepTeaBag() 的动作很类似,因此我们可以使用 brew() 来统一这两个方法;addSugarAndMilk() 与 addLemon() 也很类似,我们用 addCondiments() 来统一。
2.4 模板方法模式登场
(1) 模板方法 UML 设计 (可参考模板方法 UML 图解)
(2) 模板方法代码实现
抽象 CaffineBeverage 类
package com.jas.templatemethod;
public abstract class CaffineBeverage {
/**
* 模板方法,用作一个算法的模板,用于制作茶与咖啡
* 并定义为 final 的,防止子类覆盖
*/
final void prepareRecipe(){
boilWater();
brew();
pourInCup();
addCondiments();
}
void boilWater(){
System.out.println("煮沸水!");
}
abstract void brew();
void pourInCup(){
System.out.println("倒进杯子里!");
}
abstract void addCondiments();
}
具体 Tea 类
package com.jas.templatemethod;
public class Tea extends CaffineBeverage {
@Override
void brew() {
System.out.println("用沸水浸泡茶叶!");
}
@Override
void addCondiments() {
System.out.println("加入柠檬!");
}
}
具体 Coffee 类
package com.jas.templatemethod;
public class Coffee extends CaffineBeverage {
@Override
void brew() {
System.out.println("用沸水浸泡咖啡!");
}
@Override
void addCondiments() {
System.out.println("加入糖与牛奶!");
}
}
测试类
package com.jas.templatemethod;
public class Test {
public static void main(String[] args) {
CaffineBeverage coffee = new Coffee();
CaffineBeverage tea = new Tea();
coffee.prepareRecipe();
System.out.println();
tea.prepareRecipe();
}
}
/**
* 输出
* 煮沸水!
* 用沸水浸泡咖啡!
* 倒进杯子里!
* 加入糖与牛奶!
*
* 煮沸水!
* 用沸水浸泡茶叶!
* 倒进杯子里!
* 加入柠檬!
*/
(3) 问题总结
CaffineBeverage 类主导一切,它拥有算法,并保护这个算法 (模板方法定义为 final 的)。对于子类来说,因为 CaffineBeverage 类的存在,代码能够最大程度的复用。并且算法存在于模板方法中,更易于修改,并且扩展性更强。CaffineBeverage 类本身专注与算法本身,由子类提供完整实现。
2.5 对模板方法进行挂钩
(1) 什么是钩子
钩子 (Hook) 是一种被声明抽象类中的方法,但是只有空的或默认的实现。
钩子的存在,可以让子类有能力对算法的不同点进行挂钩。到底要不要挂钩,由子类自行决定。
(2) 将钩子应用到代码中去
改造 CaffineBeverage
package com.jas.templatemethod;
public abstract class CaffineBeverageWithHook {
final void prepareRecipe(){
boilWater();
brew();
pourInCup();
/** 这里加上钩子的判断,只有钩子返回值为 true 时,表示顾客想要加调料,才添加调料*/
if(customerWantsCondiments()){
addCondiments();
}else {
System.out.println("不添加任何调料");
}
}
void boilWater(){
System.out.println("煮沸水!");
}
abstract void brew();
void pourInCup(){
System.out.println("倒进杯子里!");
}
abstract void addCondiments();
/**
* 这是一个钩子,子类可以自行选择覆盖,但是也可以不这么做。
* @return
*/
boolean customerWantsCondiments(){
return true;
}
}
改造 Coffee
package com.jas.templatemethod;
import java.util.Scanner;
public class CoffeeWithHook extends CaffineBeverageWithHook {
@Override
void brew() {
System.out.println("用沸水浸泡咖啡!");
}
@Override
void addCondiments() {
System.out.println("加入糖与牛奶!");
}
@Override
boolean customerWantsCondiments() {
/** 只有当用户输入的是 'y' 时,返回 true,才添加调料 */
if("y".equals(getUserInput())){
return true;
}else {
return false;
}
}
/**
* 获得用户在控制台输入的数据
* @return
*/
private String getUserInput(){
String answer = null;
System.out.println("你是否想要添加调料,请输入 (y/n)");
Scanner read = new Scanner(System.in);
answer = read.nextLine();
return answer;
}
}
测试
package com.jas.templatemethod;
public class Test {
public static void main(String[] args) {
CaffineBeverageWithHook coffeeWithHook = new CoffeeWithHook();
coffeeWithHook.prepareRecipe();
}
/**
* 输出
* 煮沸水!
* 用沸水浸泡咖啡!
* 倒进杯子里!
* 你是否想要添加调料,请输入 (y/n)
* y
* 加入糖与牛奶!
*/
}
(3) 钩子总结
在实际工作中,如果算法的这个部分是可选的,那么你可以选择使用钩子。
钩子可以让子类实现算法中可选的部分,或者在钩子对子类的实现并不重要时,子类可以对此钩子置之不理。
钩子可以使子类能够有机会对模板方法中即将发生的步骤做出反应。
三、 模板方法模式总结
3.1 模板方法优缺点总结
优点
- 封装不变部分,扩展可变部分。
- 提取公共代码,便于维护。
- 行为由父类控制,子类实现。
缺点
每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。
3.2 模板方法模式知识点总结
- 模板方法”定义了算法的实现步骤,并把一些步骤的实现推迟到子类中。
- 模板方法模式为我们提供了一种代码复用的技巧。
- 模板方法的抽象类可以定义具体方法、抽象方法和钩子。抽象方法由其子类实现。
- 钩子是一种方法,它在抽象类中不做事,或者只做一些默认的事,子类可以选择要不要覆盖它。
- 为了防止子类覆盖模板方法,可以将模板方法定义为 final 的。
- 可以把工厂方法理解为模板方法的一种特殊版本。
PS:点击了解更多设计模式 http://blog.youkuaiyun.com/codejas/article/details/79236013
参考文献
《Head First 设计模式》