模板方法

思来想去,还是不使用这本书了。有点脱离实践,而且实例太少,太简单,翻来看去也看不出各个设计模式的应用面究竟在哪里……于是我又搞了一本《设计模式的艺术 软件开发人员内功修炼之道》。

这一章介绍的两个设计模式都是基于继承的。

模板方法(Template Method)模式

我的手头上有个工具(tool),它可以用来做某些事情。在使用这个工具之前,我们需要找到(find)这个工具,准备(prepare)这个工具(比如把它从一堆东西里翻出来),然后使用(use)它,最后收拾(clean)它。它用程序语言怎么去描述呢?

首先,这里的工具肯定是一个抽象的概念,不存在不指代任何具体事物的所谓“工具”,所以这里的工具必定是一个抽象类。但是,我们能够确定使用它的一个流程,所以不应当使用接口(其实使用接口和 default 方法也是可行的,但是一个问题是接口不能使用 final 关键字,即使是 default 方法。)。于是,我们可以得到如下代码——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class Tool {
public void find() {
System.out.println("寻找工具……"); // 这一步对所有工具来说都是一样的
}
public abstract void prepare(); // 如果要防止被外界直接调用,也可以使用 protected,不过这里跟着书上的来。
public abstract void use();
public abstract void clean();
public final void makeUseOf() { // 必须使用 final!否则设计模式会被破坏。
// 这里当然也可以加入更加复杂的逻辑
find();
prepare();
use();
clean();
}
}

显然,这里的抽象类 Tool 承担了模板 (Template) 的作用,它规范了任何 Tool 范畴下的事物(也就是其子类)的使用流程(即 makeUseOf 方法)。子类只需要重写(实现)prepare,use,clean 方法即可。

为什么这里的 makeUseOf 是 final 方法?因为这个流程是所有 Tool 所共有的,任何 Tool 都不应该离开这个流程。而如果不使用 final,且该方法被子类重写,这会导致从概念上来说子类已经不是这个父类的种概念了。而且这个设计模式也被破坏掉了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Wrench extends Tool{
@Override
public void prepare() {
System.out.println("准备扳手……");
}
@Override
public void use() {
System.out.println("使用扳手……");
}
@Override
public void clean() {
System.out.println("整理扳手……");
}
public static void main(String[] args) {
new Wrench().makeUseOf();
}
}

这种设计模式是对抽象类的一种应用。它和接口——实现的这种设计规则相区别的一个很大的不同是,它规定了其子类的某种使用流程,即使该流程所调用的方法都是抽象方法。这显然在现实生活中能找到充分的例子。比如上面说的 Tool,我们并不知道这 Tool 究竟指代的是哪个具体的工具,但是我们终归是能够归纳出所有 Tool 所共有的一些东西。

模板方法就是这样一种设计模式——抽象的父类定义处理流程的框架(这个流程中所使用的方法中应存在抽象方法),而子类则去实现该流程中的具体处理(其中的抽象方法)

模板方法实现了所谓的反向控制,即父类调用子类的操作,通过对子类的具体实现扩展不同的行为。

下面简单提及书中的例子。例子要求实现一个 display 程序,能对一个特定类型的数据(如字符,字符串,etc……显然对于任意种类的数据都必须单独处理,比如创建其对应的类)进行格式化输出。假设在经过一些实践后,我们对任意类型的数据(对应的 display 类)都能够归纳出一定的流程——首先创建开头,再创建五次正文(为什么是五次?问甲方去),再创建结尾。于是,我们可以应用模板方法进行这样的抽象——

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// AbstractDisplay.java
public abstract class AbstractDisplay {
protected abstract void open();
protected abstract void print();
protected abstract void close();
public final void display() {
open();
for (int i = 0; i < 5; i++)
print();
close();
}
}

// CharDisplay.java
public class CharDisplay extends AbstractDisplay{
private char ch;
public CharDisplay(char ch) {
this.ch = ch;
}
@Override
protected void open() {
System.out.print("<<");
}
@Override
protected void print() {
System.out.print(ch);
}
@Override
protected void close() {
System.out.println(">>");
}
}

//StringDisplay.java
public class StringDisplay extends AbstractDisplay {
private String str;
private int width;
public StringDisplay(String str) {
this.str = str;
this.width = str.getBytes().length; // 使用 getBytes 可以保证如果存在占两个字节大小,且字宽也两倍于等宽字体中单个字符大小的字符(比如中文,日语)也能够正常显示。
}
private void printLine() { // 辅助函数,用以生成开头一行和结尾一行
System.out.print("+");
for (int i = 0; i < width; i++)
System.out.print("-");
System.out.print("+");
}
@Override
protected void open(){
printLine();
}
@Override
protected void print(){
System.out.println("|"+str+"|");
}
@Override
protected void close(){
printLine();
}
}

Main 文件省略。使用了模板方法的好处是,如果要修改处理的流程,比如要改成 print10 次,只需要更改抽象父类即可。如果不对这流程进行抽象,每次要更改需求就需要更改所有实现类了。

模板方法的一个问题是,子类必须要了解父类中所定义的流程,也就是说要理解父类中定义的抽象方法的调用时机,否则子类的编写是比较困难的。这说明父类和子类是紧密联系的(紧耦合?)。