设计模式:模版方法模式
模版所解决的问题
在介绍设计模式之前,我们先来看一个具体的编码问题。餐馆为了保证菜品的出菜速度与质量,都会有流程化管理,对于每道菜来说,都有一系列的操作步骤,比如下面这个类表示的就是中餐的一个大致流程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ChineseDishes {
public void prepareDish() {
prepareIngredient();
cut();
fry();
putIntoPlate();
}
public void prepareIngredient() {
System.out.println("Prepare ingredient...");
}
public void cut() {
System.out.println("cut ingredient if necessary...");
}
public void fry() {
System.out.println("frying...");
}
public void putIntoPlate() {
System.out.println("plating if it is done...");
}
}
但是除了中餐外,餐馆还会准备面包点心,步骤类似,但是到具体的操作上又不太一样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class DessertDishes {
public void prepareDish() {
prepareIngredient();
mix();
bake();
putIntoPlate();
}
public void prepareIngredient() {
System.out.println("Prepare ingredient...");
}
public void mix() {
System.out.println("mix ingredient...");
}
public void bake() {
System.out.println("baking...");
}
public void putIntoPlate() {
System.out.println("plating if it is done...");
}
}
除了上面这两种料理,还会有其他的料理。每个料理的流程都大致一样,但是具体到步骤上又略有不同。如果我们针对每个料理都创建一个单独的类,那么久而久之就会产生下面的问题:
- 如果餐馆要对所有菜品增加一个步骤,那么所有的类都需要改动
- 这里有很多重复的代码,比如上面两个例子中的
prepareIngredient()
和putIntoPlate()
- 当新加一个料理,在新类中我们需要做很多的事情,没办法重用现有的代码
- 最后,也是最重要的,每个类自己管控整个流程,流程算法分散在各个类中,这给代码管控和阅读增加了难度
模版就是为了解决上述问题的,它会将上面的流程给统一起来,你也可以将模版理解为框架,框架有了,剩下的就是往框架里填充具体的内容了。让我们来看看模版方法模式具体是什么。
模版方法模式
还是上面的例子,基于观察,我们可以整理出一个适用于所有料理的操作流程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
abstract class Dishes {
final public void prepareDish() {
prepareIngredient();
before();
cook();
putIntoPlate();
}
public void prepareIngredient() {
System.out.println("Prepare ingredient...");
}
abstract void before();
abstract void cook();
public void putIntoPlate() {
System.out.println("plating if it is done...");
}
}
基本上所有的料理头一步都是把所需要的食材给准备好,所有料理的最后一步都是装盘。中间的两步,对食材做预处理以及烹饪食材,每个料理也都有,但是不同的料理会有所不同,这个基类中我们给出了 abstract 方法,让子类自己去实现。
那么这样的话,子类只需要关注 before()
和 cook()
这两个步骤,将对应的逻辑填入即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ChineseDishes extends Dishes {
public void before() {
System.out.println("cut ingredient if necessary...");
}
public void cook() {
System.out.println("frying...");
}
}
class DessertDishes extends Dishes {
public void before() {
System.out.println("mix ingredient...");
}
public void cook() {
System.out.println("baking...");
}
}
和之前的实现相比,现在这两个类是不是简单了不少?之前一个类中有 5 个方法,而现在只有 2 个。更重要的是,我们清楚地知道这两个类有着相同的框架,我们不再把类单独拆分开来看。在基类 Dishes
中,prepareDish
就是模版方法,它让整个流程变得非常的清晰。
另外,你会发现这里真正的对外接口还是 prepareDish
,它相当于这部分逻辑的 main 函数。但是在基类中,我们是将 prepareDish
设置为 final,也就是说,子类无法更改流程,子类只能给流程填充具体的细节,让最后所展现的东西有所不同。那这样做的好处是什么呢?
想象一下,在一个系统中,如果每个模块都相互关联,每个模块都可以彼此发送请求,模块与模块之间的相互关系会变得错综复杂。而模版方法模式很好地避免了这一点,因为 子类实现的方法只能由基类中的模版方法所调用,这样一来请求流就是单向的了,这其实是著名的 好莱坞设计原则。
模版方法模式的具体应用
排序
Java 中的排序方法,比如 Collections.sort()
或 Arrays.sort()
可以对任意的类进行排序,只要这个类实现了 Comparable
接口。
比如下面这个 Employee
类就可以使用排序方法来排序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Employee implements Comparable {
String name;
int salary;
public Employee(String name, int salary) {
this.name = name;
this.salary = salary;
}
public String toString() {
return name + " salary " + salary;
}
public int compareTo(Object object) {
Employee otherEmployee = (Employee)object;
return this.salary - otherEmployee.salary;
}
}
class Client {
public static void main(String[] args) {
Employee[] employees = {
new Employee("Jack", 190000),
new Employee("Eric", 150000),
new Employee("Tom", 120000),
new Employee("Steven", 240000),
new Employee("Jeff", 160000),
new Employee("Brian", 190000)
};
System.out.println("Before sorting:");
display(employees);
Arrays.sort(employees);
System.out.println("\nAfter sorting:");
display(employees);
}
public static void display(Employee[] employees) {
for (int i = 0; i < employees.length; i++) {
System.out.println(employees[i]);
}
}
}
虽然说这里并不存在类的继承,但思想依然还是模版方法模式。具体类不需要关系排序是如何进行的,排序方法也不需要关心具体类是什么,它们唯一的关联就是 Comparable
接口中 CompareTo
的实现,并且这里的关系也非常清晰,排序方法调用具体类中的 CompareTo
来进行排序。
钩子(Hook)
在实际工作中你可能经常听到一些名词,诸如,Web Hook,Hook functions,Hooking 等等。这些概念其实都是源于模版方法模式。
刚听这个名词,你可能觉得很抽象,非常不理解,Hook?钩子?什么鬼?
在我们日常生活中,钩子是用来连接两个物体的,说得通俗些,我们经常会通过钩子把一个物体挂在另一个物体上。比如晒衣服,你会将衣服挂在衣架上,还有就是你会将一些照片或者画通过钩子挂在墙上等等。
编程中,Hook 就是起这个作用,将两个模块中的代码逻辑关联起来。光说肯定很抽象,我们还是拿之前的例子来介绍:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
abstract class Dishes {
final public void prepareDish() {
prepareIngredient();
before();
cook();
if (isForHere()) {
putIntoPlate();
}
}
public void prepareIngredient() {
System.out.println("Prepare ingredient...");
}
public boolean isForHere() {
return true;
}
abstract void before();
abstract void cook();
public void putIntoPlate() {
System.out.println("plating if it is done...");
}
}
这里的 isForHere
就是一个钩子,子类可以不 override 这个方法,那么实际的流程就会以默认的步骤执行。当然子类也可以选择 override 这个方法,那么在特定的情况下可以跳过最后这个步骤。
子类可以通过实现钩子来决定是否要执行某些步骤,或者选择如何执行某些步骤。这就像是基类给了子类一个钩子,子类可以决定是否使用这个钩子,以及具体在钩子上挂什么东西。这给了子类一定的改变流程的能力,当然,这个改变仅仅针对特定的点而非整个框架。