模板方法模式(template method)
- 封装算法
文章说明
- 该文章为
《Head First 设计模式》
的学习笔记 - 非常推荐大家去买一本
《Head First 设计模式》
去看看,节奏轻松愉悦,讲得通俗易懂,非常适合想要学习、了解、应用设计模式以及OO设计原则的小白。
1. 典型的模板方法
1.1 定义模板方法
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
简而言之:模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现
1.2 模板方法的类图
(1)templateMethod()
方法创建了算法的模板,将算法定义成一组步骤,其中任何步骤都可以是抽象的,由子类负责实现。这可以确保算法的结构不变,同时由子类提供部分实现
下面展示这个算法的模板:
public void templateMethod() {
primitiveOperation1();
concreteOperation();
primitiveOperation2();
}
(2)concreteOperation()
是具体方法,让子类共享的方法,增强代码的复用。
(3)primitiveOperation1()、primitiveOperation2()
就属于抽象方法,具体实现是由子类负责。当模板方法需要这两个抽象方法就会调用它们。
1.3 逐步认识模板方法
1.3.1 饮料冲泡法
我开了一家奶茶店,现在我要求师傅按照训练手册的冲泡法步骤来制作饮料,像这样:
------------ 训练手册 ------------
-
咖啡冲泡法
- (1) 把水煮沸
- (2) 用沸水冲泡咖啡
- (3) 把咖啡倒进杯子
- (4) 加糖和牛奶 茶冲泡法
- (1) 把水煮沸
- (2) 用沸水浸泡茶叶
- (3) 把茶倒进杯子
- (4) 加柠檬
------------ ------------ ------------
根据以上的描述,我们分别创建咖啡Coffee
和茶Tea
public class Coffee {
void prepareRecipe() { // 咖啡冲泡法
boilWater();
brewCoffeeGrinds();
pourInCup();
addSugarAndMilk();
}
public void boilWater() { // 煮沸水
System.out.println("Boiling water");
}
public void brewCoffeeGrinds() { // 冲泡咖啡
System.out.println("Dripping Coffee through filter");
}
public void pourInCup() { // 倒进杯子
System.out.println("Pouring into cup");
}
public void addSugarAndMilk() { // 加糖和奶
System.out.println("Adding Sugar and Milk");
}
}
public class Tea {
void prepareRecipe() { // 柠檬茶冲泡法
boilWater();
steepTeaBag();
pourInCup();
addLemon();
}
public void boilWater() { // 煮沸水
System.out.println("Boiling water");
}
public void steepTeaBag() { // 冲茶包
System.out.println("Steeping the tea");
}
public void pourInCup() { // 倒进杯子
System.out.println("Pouring into cup");
}
public void addLemon() { // 加柠檬
System.out.println("Adding Lemon");
}
}
1.3.2 重复的代码
显而易见的,它们出现了重复的代码,咖啡和茶的boilWater()
和 pourInCup()
是完全一致的。这里我们把重复的代码抽离出来,放到基类里面去,实现代码的复用。类图如下:
1.3.3 抽象PprepareRecipe方法
这个新的设计比之前的要好,但是还没有达到一个代码最大复用,其实还可以抽象prepareRecipe()
方法,注意看:
// 柠檬茶冲泡法 // 咖啡冲泡法
void prepareRecipe() { void prepareRecipe() {
boilWater(); boilWater();
steepTeaBag(); <-----> brewCoffeeGrinds();
pourInCup(); pourInCup();
addLemon(); <-----> addSugarAndMilk();
} }
浸泡(steep)茶
和 冲泡(brew)咖啡
抽象出来都是一个概念,将它统称为冲泡brew()
; 加柠檬,加糖和牛奶也是一样,统称为加调料addCondiments()
。我们再看抽象过后的冲泡法:
// 抽象的冲泡法
void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
1.3.4 更进一步的设计
进一步的设计的类图在3. 好莱坞原则有展示。
豁然开朗!接下来我们重写抽象类 :Beverage
public abstract class Beverage { // 咖啡因饮料
// 声明为final,不让子类覆盖这个方法
final void prepareRecipe() { // 咖啡冲泡法
boilWater();
brew();
pourInCup();
addCondiments();
}
// 声明为抽象,让子类去实现
abstract void brew();
abstract void addCondiments();
// 具体方法被子类共享,实现代码复用最大化
// 可以使用final,确保方法不改变
final void boilWater() {
System.out.println("Boiling water");
}
final void pourInCup() {
System.out.println("Pouring into cup");
}
}
这里prepareRecipe()
和boilWater()
、pourInCup()
都使用了final
修饰,目的都是为了不让子类修改,确保方法不变。
子类:Tea
、Coffee
public class Coffee extends Beverage {
@Override void brew() { // 冲泡咖啡
System.out.println("Dripping Coffee through filter");
}
@Override void addCondiments() { // 加糖和奶
System.out.println("Adding Sugar and Milk");
}
}
public class Tea extends Beverage {
@Override void brew() { // 冲茶包
System.out.println("Steeping the tea");
}
@Override void addCondiments() { // 加柠檬
System.out.println("Adding Lemon");
}
}
子类只需要自行处理冲泡和添加调料的部分。
测试一下:BeverageTestDrive
public class BeverageTestDrive {
public static void main(String[] args) {
Beverage coffee = new Coffee();
Beverage tea = new Tea();
System.out.println("\nMaking coffee...");
coffee.prepareRecipe();
System.out.println("\nMaking tea...");
tea.prepareRecipe();
}
}
客户只需要对抽象类Beverage
操作即可,客户与具体的饮料类解耦了。
测试结果
Making coffee...
Boiling water
Dripping Coffee through filter
Pouring into cup
Adding Sugar and Milk
Making tea...
Boiling water
Steeping the tea
Pouring into cup
Adding Lemon
2. 钩子
2.1 认识钩子
先来看一个带钩子(hook)
的模板方法的抽象类:AbstractClass
abstract class AbstarctClass {
final void templateMethod() {
primitiveOperation1();
primitiveOperation2();
concreteOperation();
hook(); // 钩子
}
// 由具体子类实现
abstract void primitiveOperation1();
abstract void primitiveOperation2();
final void concreteOperation() {
// 在这里实现
}
// 这是一个具体的方法,但什么都不做
void hook() {}
}
可以见到有一个hook()
方法在抽象类实现,但不做任何事情,我们称这样的方法为"hook"(钩子)
。子类可以根据情况要不要覆盖它们。
2.2 挂钩
但是这有什么用呢?用处可大了,接下来我要使用1.3例子对模板方法进行挂钩,给你展示一下它的魅力!
带钩子的抽象饮料类:BeverageWithHook
public abstract class BeverageWithHook {
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
// 加上控制语句
if(customerWantsCondiments())
addCondiments();
}
// hook钩子
boolean customerWantsCondiments() {
return true; // 只返回true不做别的事
}
abstract void brew();
abstract void addCondiments();
final void boilWater() {
System.out.println("Boiling water");
}
final void pourInCup() {
System.out.println("Pouring into cup");
}
}
hook()
方法在这里只会返回true
,不做别的事
这里我们使用钩子控制了饮料是否添加调料,如果子类不提供自己的实现,抽象类就会提供一个默认的实现,这里默认添加调料。
2.3 使用钩子
这里我们询问顾客得知是否要加调料,即通过控制台输入确认。
实现了钩子的具体饮料类:CoffeeWithHook
public class CoffeeWithHook extends BeverageWithHook {
@Override void brew() {
System.out.println("Dripping Coffee through filter");
}
@Override void addCondiments() {
System.out.println("Adding Sugar and Milk");
}
// 覆盖这个钩子,提供自己的功能实现
@Override boolean customerWantsCondiments() {
String answer = getUserInput();
if(answer.toLowerCase().startsWith("y")) // y开头
return true;
else
return false;
}
/**
* 从控制台获取用户输入
*/
private String getUserInput() {
String answer = "no";
System.out.println(
"Would you like sugar and milk with your coffee (y/n)? ");
BufferedReader in =
new BufferedReader(
new InputStreamReader(System.in));
try {
answer = in.readLine();
} catch (IOException e) {
System.err.println("IO error trying to read your answer");
}
return answer;
}
}
关于这个获取用户输入的方法getUserInput
,可以有更好的处理,比如可以将getUserInput()
方法实现在抽象父类以实现代码的复用,或者委托给别的对象处理,这样更有弹性。
测试一下:BeverageTestDrive2
public class BeverageTestDrive2 {
public static void main(String[] args) {
CoffeeWithHook coffee = new CoffeeWithHook();
TeaWithHook tea = new TeaWithHook();
System.out.println("\nMaking coffee...");
coffee.prepareRecipe();
System.out.println("\nMaking tea...");
tea.prepareRecipe();
}
}
测试结果:
Making coffee...
Boiling water
Dripping Coffee through filter
Pouring into cup
Would you like sugar and milk with your coffee (y/n)?
y
Adding Sugar and Milk
Making tea...
Boiling water
Steeping the tea
Pouring into cup
Would you like lemon with your coffee (y/n)?
n
钩子竟然能作为控制条件,影响抽象类中的算法流程,非常的酷!
关于使用钩子的真正目的?
(1)让子类实现算法可选的部分,或者保持默认实现。
(2)让子类能够有机会对模板方法中某些即将发生的(或刚刚发生的)步骤做反应。
3. 好莱坞原则
别调用我们,我们会调用你。
避免让高层和底层组件之间有明显的环状依赖。
我们通过1.3.4例子的类图了解好莱坞原则:
(1)Beverage
高层组件,能够控制冲泡法的算法流程,只有在需要子类实现某个方法的时候才调用子类。
(2)Tea、Coffee
低层组件如果没有先被调用,绝对不会直接调用抽象类。
与依赖倒置原则之间的关系?
-
依赖倒置原则
- 教我们尽量避免使用具体类,而多使用抽象类。即尽量避免依赖。 好莱坞原则
- 是在创建框架或组件的技巧,教我们建立好的依赖。允许底层结构能够互相操作,二又防止其他类太过依赖他们。
4. Java API的模板方法
这个模式很常见是因为对创建框架来说,这个模式简直棒极了。由框架控制如何做事情,而由你(客户端程序员)指定框架算法中的每个步骤的细节。
4.1 Arrays.sort()排序
先来看部分Arrays.sort()
源码(简化版):
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(dest[j])>0; j--)
swap(dest, j, j-1);
return;
}
(1)把mergeSort方法
看做是模板方法,定义了算法流程;
(2)swap方法
则是一个具体方法,已经在Arrays类
中实现;
(3)compareTo方法
则是"抽象"方法,想要使用sort()方法
,必须填该补模板方法的空缺,即实现Comparable接口
并实现compareTo()方法
。
你有可能会疑惑,这是模板方法吗?我都没有使用继承。
这个Array类sort()方法的设计者受到一些约束,我们无法继承一个数组,而sort()方法希望能适用于所有的数组(每个数组都是不同的类)。所以sort()定义为静态方法,而由被排序的对象内的每个元素自行提供比较大小的算法部分。不是典型的模板方法,但是符合模板方法的精神。而且不需要继承就可以使用这个算法,这样使得排序变得更有弹性、更有用。
实现排序
比较鸭子:Duck
public class Duck implements Comparable<Duck> {
String name; // 名字
int weight; // 体重
public Duck(String name, int weight) {
this.name = name;
this.weight = weight;
}
public String toString() {
return name + " " + weight;
}
// 该方法指定了鸭子如何比较,这里我使用了根据体重比较
public int compareTo(Duck o) { // 升序
return (weight < o.weight ? -1 :
(weight == o.weight ? 0 : 1));
}
}
测试排序:DuckSortTestDrive
public class DuckSortTestDrive {
public static void main(String[] args) {
Duck[] ducks = {
new Duck("Daffy", 8),
new Duck("Dewey", 6),
new Duck("Howard", 9),
new Duck("Louie", 8),
new Duck("Donald", 10),
new Duck("Huey", 2)
};
System.out.println("Before sorting:");
System.out.println(Arrays.asList(ducks));
// ducks数组元素必须实现了Comparable接口
Arrays.sort(ducks);
System.out.println("\nAfter sorting:");
System.out.println(Arrays.asList(ducks));
}
}
测试结果:
Before sorting:
[Daffy 8, Dewey 6, Howard 9, Louie 8, Donald 10, Huey 2]
After sorting:
[Huey 2, Dewey 6, Daffy 8, Louie 8, Howard 9, Donald 10]
4.2 Swing
JFrame类
中,在默认的状态下,paint()
是不做事情的,因为他是一个钩子hook
。
public class MyFrame extends JFrame {
public MyFrame(String title) { // 初始化动作
super(title);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(300, 300);
setVisible(true);
}
// 钩子
@Override
public void paint(Graphics g) {
super.paint(g);
String msg = "I rule!!";
g.drawString(msg, 100, 100);
}
public static void main(String[] args) {
new MyFrame("Hook");
}
}
4.3 Applet
具体的Applet大量使用钩子来提供行为。因为这些行为都是作为钩子实现的,所有Applet类就不去实现他们。
你可以去看看Applet的源码,有很多默认不事情的钩子。
接下来让我们看看其中几个钩子:
public class MyApplet extends Applet {
String message;
@Override
public void init() {
message = "Hello World, I'm alive";
repaint();
}
@Override
public void start() {
message = "Now I'm starting up...";
repaint();
}
@Override
public void stop() {
message = "Oh, now I'm being stopped...";
repaint();
}
@Override
public void destroy() {
// applet 正在被销毁
}
@Override
public void paint(Graphics g) {
g.drawString(message, 5, 15);
}
}
MyApplet类
中覆盖重写的方法全都是钩子hook
,它们在特定的时期被执行。
Java API 中还有别的模板方法的例子,比如:
java.io
的nputStream类
中有一个read方法
,是由子类去实现的,而这个方法会被read(byte b[], int off, int len)模板方法
使用。
5. 要点
- 模板方法 定义了算法的步骤,把这些步骤实现延迟到了子类。
- 模板方法的抽象类可以定义具体方法、抽象方法和钩子。
- 钩子是一种方法,在抽象类中不做事,或者只做默认的事,子类可以选择要不要去覆盖它。
- 为了防止子类改变模板方法中的算法,可以将模板方法声明为
final
。 - 好莱坞原则告诉我们,将决策权放在高层模块中,以便决定如何以及何时调用底层模块。
- 真实世界代码可以看到模板方法模式的许多变体。
- 策略模式 和 模板方法模式都封装算法,一个用组合,一个用继承。
- 工厂方法是模板方法的一种特殊版本。