1. 模板方法
模板方法:在一个方法中定义一个算法的骨架(步骤),而将一些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
模板方法的类图:
2. 实现
假设有一家店卖咖啡和茶,对于咖啡的冲泡法有4个步骤:
- 把水煮沸;
- 咖啡滴过过滤器;
- 倒进杯子;
- 加糖。
而茶的冲泡法也有4个步骤:
- 把水煮沸;
- 用沸水冲泡茶包;
- 倒进杯子;
- 加柠檬。
现在来设计这两个类,从上面的步骤来分析两个类,第一个步骤和第三个步骤是一样的,而第二和第四个步骤是完全不同,如果设计一个抽象类(CaffeineBeverage),那么该步骤可以交给子类自己去实现,而相同的方法可以封装在一起。
这四个步骤的顺序不变,所以可以在超类中把四个步骤封装起来成为一个方法,并使用final修饰,让子类不能修改。该方法就是模板方法。
当然如果把这两个类独立开来设计也是可以的,但是它们之间存在很多重复代码,而且不具备弹性,就是如果新增饮料种类,那么所有的代码都得重新写(包含重复代码),如果使用模板方法,则是提供一个框架,新增种类只需要继承CaffeineBeverage即可。
public abstract class CaffeineBeverage {
final void prepareRecipe() {
boilWater();
brew();
pourIncup();
addCondiment();
}
abstract void brew();
abstract void addCondiment();
void boilWater() {
System.out.println("把水煮沸");
}
void pourIncup() {
System.out.println("倒进杯子");
}
}
茶:
public class Tea extends CaffeineBeverage {
@Override
void brew() {
System.out.println("冲茶包");
}
@Override
void addCondiment() {
System.out.println("添加柠檬");
}
}
咖啡
public class Coffee extends CaffeineBeverage {
@Override
void brew() {
System.out.println("咖啡滴过过滤器");
}
@Override
void addCondiment() {
System.out.println("添加糖");
}
}
客户端:
public class Client {
public static void main(String[] args) {
Tea tea = new Tea();
tea.prepareRecipe();
Coffee coffee = new Coffee();
coffee.prepareRecipe();
}
}
输出:
其实模板方法模式中还有一个方法名为钩子(hook),钩子是一种被声明在抽象方法中的方法,但只有空的或默认的实现,子类可以重新覆写。钩子的存在可以让子类有能力对算法的不同点进行挂钩。
以下是钩子的作用之一:
public abstract class CaffeineBeverage {
final void prepareRecipe() {
boilWater();
brew();
pourIncup();
if(hook()) { // 使用钩子来进行判断是否jia'le
addCondiment();
}
}
abstract void brew();
abstract void addCondiment();
void boilWater() {
System.out.println("把水煮沸");
}
void pourIncup() {
System.out.println("倒进杯子");
}
boolean hook() {
return true;
}
}
public class CoffeeWithHook extends CaffeineBeverage {
@Override
void brew() {
System.out.println("咖啡滴过过滤器");
}
@Override
void addCondiment() {
System.out.println("添加糖");
}
public boolean hook() {
return getUserInput();
}
private boolean getUserInput() {
System.out.println("是否加糖(y/n)?");
Scanner scanner = new Scanner(System.in);
String answer = scanner.next();
scanner.close();
if(answer.equalsIgnoreCase("y")) {
return true;
} else {
return false;
}
}
}
客户端:
public class Client {
public static void main(String[] args) {
CoffeeWithHook coffee = new CoffeeWithHook();
coffee.prepareRecipe();
}
}
输出:
把水煮沸
咖啡滴过过滤器
倒进杯子
是否加糖(y/n)?
n
把水煮沸
咖啡滴过过滤器
倒进杯子
是否加糖(y/n)?
y
添加糖
钩子的用法:
- 如上面一样,对某部分可选,就可以使用。
- 让子类能够有机会对模板方法中某些即将发生的步骤做出反应。
3. Java中的模板方法模式
在java中的模板方法模式跟上面的有点不同,比如Arrays.sort()(上次听马士兵老师的策略模式,他讲的就是用sort,所以sort到底使用的是模板方法模式还是策略模式?在《HeadFirst设计模式》中也有提到这一点,它的结论就是:认为sort使用的更像是模板方法模式。因为对于策略模式,所组合的类实现了整个算法。而数组所实现的排序算法并不完整,因为还需要有一个compareTo()方法,所以更认为是模板方法),这个静态方法接收类型的数据,但是可能需要实现Comparable接口。
刚刚去看了下代码,发现java8的Arrays.sort()内部使用的是快排(准确说应该是双轴快排),不过像上面的归并排序还存在,只不过名称改为legacyMergeSort()。
模板方法是定义在超类中,而这个跟模板方法说的不太一样,legacyMergeSort()的设计者想把这个方法能适用于任何类型的数组,所以把legacyMergeSort()方法改为静态。它使用的起来和它被定义在超类中是一样的,而且还有一个细节:因为legacyMergeSort()方法并不是真正定义在超类中,所以legacyMergeSort()方法需要知道所传入的数组对象已经实现了Comparable接口和覆写compareTo()方法,否则无法排序。其实现在使用快排的sort也是需要数组对象实现实现了Comparable接口和覆写compareTo()方法。
至于例子就算了,太简单了。
类似的还有java.io的InputStream类中的read()方法,由子类来实现,该方法又被read(byte b[], int off, int len)模板方法使用。
以下是三种设计模式的比较:
模板方法模式:子类决定如何实现算法中的某些步骤。
策略模式:封装可互换的行为,然后使用委托来决定要采用哪一个行为,这些行为都是完整的。
工厂方法:要创建哪个具体类。
4. 总结
转自菜鸟教程
意图: 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
主要解决: 一些方法通用,却在每一个子类都重新写了这一方法。
何时使用: 有一些通用的方法。
如何解决: 将这些通用算法抽象出来。
关键代码: 在抽象类实现,其他步骤在子类实现。
应用实例:
-
在造房子的时候,地基、走线、水管都一样,只有在建筑的后期才有加壁橱加栅栏等差异。
-
西游记里面菩萨定好的 81 难,这就是一个顶层的逻辑骨架。
-
spring 中对 Hibernate 的支持,将一些已经定好的方法封装起来,比如开启事务、获取 Session、关闭 Session 等,程序员不重复写那些已经规范好的代码,直接丢一个实体就可以保存。
优点:
- 封装不变部分,扩展可变部分。
- 提取公共代码,便于维护。
- 行为由父类控制,子类实现。
缺点: 每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。
使用场景: 1、有多个子类共有的方法,且逻辑相同。 2、重要的、复杂的方法,可以考虑作为模板方法。
注意事项: 为防止恶意操作,一般模板方法都加上 final 关键词。
参考:
《HeadFirst设计模式》