模板的原意是指带有镂空文字的薄薄的塑料板。只要用笔在模板的镂空处进行临摹,即使是手写也能写出整齐的文字,但是具体写出的文字是什么感觉则依赖于所用的笔。如果使用签字笔来临摹,则可以写出签字似的文字;如果使用铅笔来临摹,则可以写出铅笔字;而如果是用彩色笔临摹,则可以写出彩色的字。但是无论使用什么笔,文字的形状都会与模板上镂空处的形状一致。
本文中所要学习的Template Method模式是带有模板功能的模式,组成模板的方法被定义在父类中。由于这些方法是抽象方法,所以只查看父类的代码是无法知道这些方法最终会进行何种具体处理的,唯一能知道的就是父类是如何调用这些方法的。
实现上述这些抽象方法的是子类。在子类中实现了抽象方法也就决定了具体的处理。也就是说,只要在不同的子类中实现不同的具体处理,当父类的模板方法被调用时程序行为也会不同。但是,不论子类中的具体实现如何,处理的流程都会按照父类中所定义的那样进行。
像这样在父类中定义处理流程的框架,在子类中实现具体处理的模式就称为Template Method模式。
用一句话来概括:将具体的处理交给子类。
这里的示例程序是一段将字符和字符串循环显示5次的简单程序。
类的功能:
类图:
通过查看AbstractDisplay类的代码,我们可以知道这3个方法都是抽象方法。也就是说,如果仅仅查看AbstractDisplay类的代码,我们无法知道这3个方法中到底进行了什么样的处理。这是因为open方法、print方法、close方法的实际处理被交给了AbstractDisplay类的子类。
这里将display用final来修饰,就是表示子类不能重写display方法。
public abstract class AbstractDisplay {
public abstract void open();
public abstract void print();
public abstract void close();
public final void display() {
open();
for (int i = 0; i < 5; i++) {
print();
}
close();
}
}
我们来看看子类之一的charDisplay类。由于CharDisplay类实现了父类AbstractDisplay类中的3个抽象方法 open、print、close,因此它并不是抽象类。这样,当dipslay方法被调用时,比如传入一个H,最终显示出来的会是:<
public class CharDisplay extends AbstractDisplay{
private char ch;
public CharDisplay(char ch) {
this.ch = ch;
}
@Override
public void open() {
System.out.print("<<");
}
@Override
public void print() {
System.out.print(ch);
}
@Override
public void close() {
System.out.println(">>");
}
}
让我们看看另外一个子类——StringDisplay类。与CharDisplay类一样,它也实现了open、 print、 close方法。
此时,如果dipslay方法被调用,结果会如何呢?假设我们向charDisplay的构造函数中传递的参数是"Hello,world ,"这个字符串,那么最终结果会像下面这样:
public class StringDisplay extends AbstractDisplay{
private String string;
private int width;
public StringDisplay(String string) {
this.string = string;
this.width = string.getBytes().length;
}
@Override
public void open() {
printLine();
}
@Override
public void print() {
System.out.println("|" + string + "|");
}
@Override
public void close() {
printLine();
}
private void printLine() {
System.out.print("+");
for (int i = 0; i < width; i++) {
System.out.print("-");
}
System.out.println("+");
}
}
在该类中生成了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("你好,世界。");
//虽然都调用的是display方法,但是实际的程序行为
//取决于CharDisplay和StringDisplay的具体实现
d1.display();
d2.display();
d3.display();
}
}
虽然都调用的是display方法,但是实际的程序行为取决于CharDisplay和StringDisplay的具体实现 。
使用Template Method模式究竟能带来什么好处呢?这里,它的优点是由于在父类的模板方法中编写了算法,因此无需在每个子类中再编写算法。
例如,我们没使用Template Method模式,而是使用文本编辑器的复制和粘贴功能编写了多个ConcreteClass角色。此时,会出现ConcreteClass1、ConcreteClass2、Concreteclass3等很多相似的类。编写完成后立即发现了Bug还好,但如果是过一段时间才发现在Concreteclass1中有Bug,该怎么办呢?这时,我们就必须将这个Bug 的修改反映到所有的ConcreteClass角色中才行。
而如果是使用Template Method模式进行编程,当我们在模板方法中发现 Bug时,只需要修改模板方法即可解决问题。
在Template Method模式中,父类和子类是紧密联系、共同工作的。因此,在子类中实现父类中声明的抽象方法时,必须要理解这些抽象方法被调用的时机。在看不到父类的源代码的情况下,想要编写出子类是非常困难的。
在示例程序中,不论是CharDisplay的实例还是StringDisplay 的实例,都是先保存在AbstractDisplay类型的变量中,然后再来调用display方法的。
使用父类类型的变量保存子类实例的优点是,即使没有用instanceof等指定子类的种类,程序也能正常工作。
无论在父类类型的变量中保存哪个子类的实例,程序都可以正常工作,这种原则称为里氏替换原则(The Liskov Substitution Principle,LSP )。当然,LSP并非仅限于Template Method模式,它是通用的继承原则。
Factory Method模式是将Template Method模式用于生成实例的一个典型例子。
设计模式学习(七):Factory Method工厂模式_玉面大蛟龙的博客-CSDN博客
在Template Method模式中,可以使用继承改变程序的行为。这是因为Template Method模式在父类中定义程序行为的框架,在子类中决定具体的处理。
与此相对的是Strategy模式,它可以使用委托改变程序的行为。与Template Method模式中改变部分程序行为不同的是,Strategy模式用于替换整个算法。
设计模式学习(四):Strategy策略模式_玉面大蛟龙的博客-CSDN博客
题目:
Java中的接口与抽象类很相似。接口同样也是抽象方法的集合,但是在TemplateMethod模式中,我们却无法使用接口来扮演AbstractClass角色,请问这是为什么呢?
答案:
这是因为TemplateMethod模式中的AbstractClass角色必须实现处理的流程。在抽象类中可以实现一部分方法(例如AbstractDisplay类中的display方法),但是在接口中是无法实现方法的。因此,在TemplateMethod模式中,无法用接口替代抽象类。