3.1 Template Method 模式
什么是模板 ?
模板的原意是指带有镂空文字的薄薄的塑料板。只要用笔在模板的镂空处进行临摹,即使是手写也能写出整齐的文字。虽然只要看到这些镂空的洞,我们就可以知道能写出哪些文字,但是具体写出的文字是什么感觉则依赖于所用的笔。如果使用签字笔来临摹,则可以写出签字似的文字;如果使用铅笔来临摹,则可以写出铅笔字;而如果是用彩色笔临摹,则可以写出彩色的字。但是无论使用什么笔,文字的形状都会与模板上镂空处的形状一致。
什么是Template Method 模式 ?
Template Method 模式是带有模板功能的模式,组成模板的方法被定义在父类中。由于这些方法是抽象方法,所以只查看父类的代码是无法知道这些方法最终会进行何种具体处理的,唯一能知道的就是父类是如何调用这些方法的。
实现上述这些抽象方法的是子类。在子类中实现了抽象方法也就决定了具体的处理。也就是说,只要在不同的子类中实现不同的具体处理,当父类的模板方法被调用时程序行为也会不同。但是,不论子类中的具体实现如何,处理的流程都会按照父类中所定义的那样进行。
像这样在父类中定义处理流程的框架,在子类中实现具体处理的模式就称为 Template Method 模式。
3.2 示例程序
这里的示例程序是一段将字符和字符串循环显示 5 次的简单程序。
在示例程序中会出现 AbstractDisplay、CharDisplay、StringDisplay、Main 这 4 个类
示例程序类图:
在 AbstractDisplay 类中定义了 display 方法,而且在该方法中依次调用了 open、print、close 这 3 个方法。虽然这 3 个方法已经在 AbstractDisplay 中被声明了,但都是没有实体的抽象方法。这里,调用抽象方法的 display 方法就是模板方法。
类的一览表:
名字 | 说明 |
---|---|
AbstractDisplay | 只实现了 display 方法的抽象类 |
CharDisplay | 实现了 open、print、close 方法的类 |
StringDisplay | 实现了 open、print、close 方法的类 |
Main | 测试程序行为的类 |
AbstractDisplay 类
public abstract class AbstractDisplay { // 抽象类AbstractDisplay
public abstract void open(); // 交给子类去实现的抽象方法(1)open
public abstract void print(); // 交给子类去实现的抽象方法(2) print
public abstract void close(); // 交给子类去实现的抽象方法(3) close
public final void display() { // 本抽象类中实现的display 方法
open(); // 首先打开……
for (int i = 0; i < 5; i++) { // 循环调用5 次print……
print();
}
close(); // ……最后关闭。这就是display 方法所实现的功能
}
}
CharDisplay 类
CharDisplay 类中的 open、print、close 方法的处理如表 3-2 所示。
方法名 | 处理 |
---|---|
open | 显示字符串 “<<” |
显示构造函数接收的 1 个字符 | |
close | 显示字符串 “>>” |
public class CharDisplay extends AbstractDisplay { // CharDisplay 是AbstractDisplay 的子类
private char ch; // 需要显示的字符
public CharDisplay(char ch) { // 构造函数中接收的字符被
this.ch = ch; // 保存在字段中
}
public void open() { // open 在父类中是抽象方法
// 此处重写该方法
System.out.print("<<"); // 显示开始字符"<<"
}
public void print() { // 同样地,此处重写print 方法
// 该方法会在display 中被重复调用
System.out.print(ch); // 显示保存在字段ch 中的字符
}
public void close() { // 同样地,此处重写close 方法
System.out.println(">>"); // 显示结束字符">>"
}
}
StringDisplay 类
StringDisplay 类中的 open、print、close 方法的处理如表 3-3 所示。
方法名 | 处理 |
---|---|
open | 显示字符串 “±----+” |
在构造函数接收的字符串前后分别加上 " | |
close | 显示字符串 “±----+” |
public class StringDisplay extends AbstractDisplay { // StringDisplay 也是
// AbstractDisplay 的子类
private String string; // 需要显示的字符串
private int width; // 以字节为单位计算出的字符串长度
public StringDisplay(String string) { // 构造函数中接收的字符串被
this.string = string; // 保存在字段中
this.width = string.getBytes().length; // 同时将字符串的字节长度也
// 保存在字段中,以供后面使用
}
public void open() { // 重写的open 方法
printLine(); // 调用该类的printLine 方法画线
}
public void print() { // print 方法
System.out.println("|" + string + "|"); // 给保存在字段中的字符串前后分别加上"|"
// 并显示出来
}
public void close() { // close 方法与
printLine(); // open 方法一样,调用printLine 方法画线
}
private void printLine() { // 被open 和close 方法调用
// 由于可见性是private,因此只能在本类中被调用
System.out.print("+"); // 显示表示方框的角的"+"
for (int i = 0; i < width; i++) { // 显示width 个"-"
System.out.print("-"); // 组成方框的边框
}
System.out.println("+"); // 显示表示方框的角的"+"
}
}
Main 类
public class Main {
public static void main(String[] args) {
// 生成一个持有'H' 的CharDisplay 类的实例
AbstractDisplay d1 = new CharDisplay('H');
// 生成一个持有"Hello, world." 的StringDisplay 类的实例
AbstractDisplay d2 = new StringDisplay("Hello, world.");
// 生成一个持有" 你好,世界。" 的StringDisplay 类的实例
AbstractDisplay d3 = new StringDisplay(" 你好,世界。");
d1.display(); // 由于d1、d2 和d3 都是AbstractDisplay 类的子类
d2.display(); // 可以调用继承的display 方法
d3.display(); // 实际的程序行为取决于CharDisplay 类和StringDisplay 类的具体实现
}
}
3.3 Template Method 模式中的登场角色
在 Template Method 模式中有以下登场角色。
- AbstractClass(抽象类)
AbstractClass 角色不仅负责实现模板方法,还负责声明在模板方法中所使用到的抽象方法。这些抽象方法由子类 ConcreteClass 角色负责实现。在示例程序中,由 AbstractDisplay 类扮演此角色。
- ConcreteClass(具体类)
该角色负责具体实现 AbstractClass 角色中定义的抽象方法。这里实现的方法将会在 AbstractClass 角色的模板方法中被调用。在示例程序中,由 CharDisplay 类和 StringDisplay 类扮演此角色。
Template Method 模式的类图
3.4 延伸阅读:类的层次与抽象类
父类对子类的要求
我们在理解类的层次时,通常是站在子类的角度进行思考的。也就是说,很容易着眼于以下几点。
- 在子类中可以使用父类中定义的方法
- 可以通过在子类中增加方法以实现新的功能
- 在子类中重写父类的方法可以改变程序的行为
现在,让我们稍微改变一下立场,站在父类的角度进行思考。在父类中,我们声明了抽象方法,而将该方法的实现交给了子类。换言之,就程序而言,声明抽象方法是希望达到以下目的。
- 期待子类去实现抽象方法
- 要求子类去实现抽象方法
也就是说,子类具有实现在父类中所声明的抽象方法的责任。因此,这种责任被称为“子类责任”(subclass responsibility)。
抽象类的意义
对于抽象类,我们是无法生成其实例的。在初学抽象类时,有人会有这样的疑问:“无法生成实例的类到底有什么作用呢?”在学完了 Template Method 模式后,大家应该能够稍微理解抽象类的意义了吧。由于在抽象方法中并没有编写具体的实现,所以我们无法知道在抽象方法中到底进行了什么样的处理。但是我们可以决定抽象方法的名字,然后通过调用使用了抽象方法的模板方法去编写处理。虽然具体的处理内容是由子类决定的,不过在抽象类阶段确定处理的流程非常重要。
父类与子类之间的协作
父类与子类的相互协作支撑起了整个程序。虽然将更多方法的实现放在父类中会让子类变得更轻松,但是同时也降低了子类的灵活性;反之,如果父类中实现的方法过少,子类就会变得臃肿不堪,而且还会导致各子类间的代码出现重复。
在 Template Method 模式中,处理的流程被定义在父类中,而具体的处理则交给了子类。但是对于“如何划分处理的级别,哪些处理需要由父类完成,哪些处理需要交给子类负责”并没有定式,这些都需要由负责程序设计的开发人员来决定。