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 方法就是模板方法。
实际上实现了 open、print、close 这3个抽象方法的是 AbstractDisplay 的子类 CharDisplay 类和 StringDisplay 类。
|| AbstractDisplay 类
AbstractDisplay 类有4个方法。其中只有 dispaly 方法实现了,其他3个均是抽象方法。查看 display 方法,发现进行了以下处理。
- 调用了 open 方法
- 调用了5次 print 方法
- 调用了 close 方法
这3个方法均是抽象方法,仅仅查看 AbstractDisplay 类,无法知道这3个方法中到底进行了什么样的处理。因为 open、print、close 方法实际处理被交给了 AbstractDisplay 的子类。
/**
*
* describe AbstractDisplay 抽象类
* @author xmc
* @date 2019/3/22 9:40
*/
public abstract class AbstractDisplay {
abstract void open();
abstract void print();
abstract void close();
// 调用一次 open ,五次 print, 一次 close
void display() {
open();
for (int i = 0; i < 5; i++) {
print();
}
close();
}
}
|| CharDisplay 类
CharDisplay 类实现了父类 AbstractDisplay 类中的3个抽象方法。
/**
*
* describe 模板方法实现类1
* @author xmc
* @date 2019/3/22 9:56
*/
public class CharDisplay extends AbstractDisplay{
private char ch;
public CharDisplay(char ch) {
this.ch = ch;
}
@Override
void open() {
System.out.print("<<<");
}
@Override
void print() {
System.out.print(ch);
}
@Override
void close() {
System.out.println(">>>");
}
}
|| StringDisplay 类
与 CharDisplay 类似,也实现了父类的3个抽象方法。
/**
*
* describe 模板方法实现类2
* @author xmc
* @date 2019/3/22 10:23
*/
public class StringDisplay extends AbstractDisplay{
private String str;
private int len;
public StringDisplay(String str) {
this.str = str;
// 获取字符串的长度,便于输出相应的格式
len = str.getBytes(StandardCharsets.UTF_8).length;
}
@Override
void open() {
printLine();
}
@Override
void print() {
System.out.println("|" + str + "|");
}
@Override
void close() {
printLine();
}
private void printLine() {
System.out.print("+");
for (int i = 0; i < len; i++) {
System.out.print("-");
}
System.out.println("+");
}
}
|| Main 类
Main 类作用测试程序行为。该类中生成了 CharDisplay 类和 StringDisplay 类的实例,并调用 display 方法。
public class Main {
public static void main(String[] args) {
AbstractDisplay d1 = new CharDisplay('H');
AbstractDisplay d2 = new StringDisplay("Hello World");
AbstractDisplay d3 = new StringDisplay("你好,世界。");
d1.display();
d2.display();
d3.display();
}
}
结果:
<<<HHHHH>>>
+-----------+
|Hello World|
|Hello World|
|Hello World|
|Hello World|
|Hello World|
+-----------+
+------------------+
|你好,世界。|
|你好,世界。|
|你好,世界。|
|你好,世界。|
|你好,世界。|
+------------------+
3.3 Template Method 模式中的登场角色
在 Template Method 模式中以下登场角色。
- AbstractClass (抽象类)
AbstractClass 角色不仅负责实现模板方法,还负责声明在模板方法中所使用到的抽象方法。这些抽象方法由子类 ConcreteClass 角色负责实现。在示例程序中,由 AbstractDisplay 类扮演此角色。 - ConcreteClass (具体类)
该角色负责具体实现 AbstractClass 角色中定义的抽象方法。这里实现的方法将会在 AbstractClass 角色的模板方法中被调用。在示例程序中,由 CharDisplay 类和 StringDisplay 类扮演此角色。
3.4 拓展思路的要点
|| 可以使逻辑处理通用化
使用 Template Method 模式的优点是由于在父类的模板方法中编写了算法,因此无需在每个子类中再编写算法。
例如,我们没使用 Template Method 模式,而是使用文本编辑器的复制和粘贴功能编写了多个 ConcreteClass 角色。此时,会出现 ConcreteClass1、2、3 等类似的类。如果发现其中有 Bug,这时,我们就必须将这个 Bug 的修改反馈到所有的 ConcreteClass 角色中才行。如果使用了 Template Method 模式编程,当我们发现模板方法中出现 Bug 时,只需要修改模板方法即可解决问题。
|| 父类与子类之间的协作
在 Template Method 模式中,父类和子类是紧密联系、共同工作的。因此,在子类中实现父类中声明的抽象方法时,必须要理解这些抽象方法被调用的时机。在看不到父类的源代码的情况写下,想要编写出子类是非常困难的。
|| 父类与子类的一致性
在示例程序中,都是先保存在 AbstractDisplay 类型的变量中,然后再来调用 display 方法的。使用父类类型的变量保存子类实例的优点是,即使没有用 instanceof 等指定子类的种类,程序也能正常工作。
无论在父类类型的变量中保存哪个子类的实例,程序都可以正常工作,这种原则称为“里氏替换原则(The Liskov Substitution Principle, LSP)”。当然,LSP 并非仅限于 Template Method 模式,它是通用的继承原则。
3.6 类的层次与抽象类
|| 父类对子类的要求
我们理解类的层次时,通常站在子类的角度进行思考。很容易着眼于以下几点。
- 在子类中可以使用父类中定义的方法
- 可以通过在子类中增加方法以实现新的功能
- 在子类中重写父类的方法来改变程序的行为
现在,让我们站在父类的角度进行思考。在父类中,我们声明了抽象方法,而将该方法的实现交给了子类。换言之,就程序而言,声明抽象方法是希望达到以下目的。
- 期待子类去实现抽象方法
- 要求子类去实现抽象方法
也就是说,子类具有实现在父类中所声明的抽象方法的责任。因此,这种责任被称为“子类责任”。
|| 父类与子类之间的协作
父类与子类的相互协作支持起了整个程序。虽然将更多方法的实现放在父类中会让子类变得更轻松,但是同时也降低了子类的灵活性;反之,如果父类中实现的方法过少,子类就会变得臃肿不堪,而且还会导致个子类间的代码出现重复。
在 Template Method 模式中,处理的流程被定义在父类中,而具体的处理则交给了子类。但是,对于 “如何划分处理的级别,哪些处理需要由父类完成,哪些处理需要交给子类负责” 并没有定式,需要由负责程序设计的开发人员来决定。
|| 抽象类的定义
对于抽象类,我们是无法生成其实例的。“无法生成实例的类到底有什么作用呢?”,这里大家应该稍微理解抽象类的意义了吧。由于在抽象方法中并没有编写具体的实现,所以我们无法知道在抽象方法中到底进行了什么样的处理。但是,我们可以决定抽象方法的名字,然后通过调用使用了抽象方法的模板方法去编写处理。虽然具体的内容由子类决定的,不过在抽象阶段确定处理的流程非常重要。