一、定义
1. 概念
模板方法模式:在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法中的某些特定步骤。
这个模式是用来创建一个算法的模板。什么是模板?如你所见的,模板就是一个方法。更具体的说,这个方法将算法定义成一组步骤,其中的任何步骤都可以是抽象的,由子类负责实现。这可以确保算法的结构保持不变,同时由子类提供部分实现。
让我们看看类图:

让我们来看看这个类图的代码:
csharp
/**
* 这个类被声明为抽象,用来作为基类,其子类必须实现其操作
*/
public abstract class AbstractClass {
/**
* 这个是模板方法。它被声明为final,以免子类改变这个算法的顺序。
* 模板方法定义了一连串的步骤,每个步骤由一个方法代表
*/
final void templateMethod() {
primitiveOperation1();
primitiveOperation2();
concreteOperation();
hook();
}
/**
* 具体子类必须实现它
*/
protected abstract void primitiveOperation1();
/**
* 具体子类必须实现它
*/
protected abstract void primitiveOperation2();
/**
* 这个方法声明为private的,子类就无法覆盖该方法,但是子类可以直接使用它
*/
private void concreteOperation() {
// 这里是实现
}
/**
* 这是一个钩子方法,它可以什么都不做,或者做一些默认的事情。
* 子类可以视情况决定要不要覆盖它们。
*/
protected void hook() {
}
}
这是一个制作茶饮料与咖啡饮料的例子
csharp
public abstract class CaffineBeverageWithHook {
void prepareRecipe() {
boilWater();
brew();
pourInCup();
if (customerWantsCondiments()) {
addCondiments();
}
}
protected abstract void brew();
protected abstract void addCondiments();
private void boilWater() {
System.out.println("Boiling water");
}
private void pourInCup() {
System.out.println("Pouring cup");
}
/**
* 这就是一个钩子,子类可以覆盖这个方法,但不见得一定要这么做
* @return
*/
protected boolean customerWantsCondiments() {
return true;
}
}
typescript
public class CoffeeWithHook extends CaffineBeverageWithHook {
@Override
protected void brew() {
System.out.println("Dripping Coffee through filter");
}
@Override
protected void addCondiments() {
System.out.println("Adding sugar and milk");
}
/**
* 覆盖了这个钩子,提供了自己的功能
* @return
*/
@Override
protected boolean customerWantsCondiments() {
String answer = getUserInput();
if (answer.toLowerCase().startsWith("y")) {
return true;
} else {
return false;
}
}
private String getUserInput() {
return "user answer";
}
}
scala
public class TeaWithHook extends CaffineBeverageWithHook {
@Override
protected void brew() {
System.out.println("Dripping Tea through filter");
}
@Override
protected void addCondiments() {
System.out.println("Adding sugar");
}
}
csharp
/**
* 测试类
*/
public class BeverageTest {
public static void main(String[] args) {
TeaWithHook teaWithHook = new TeaWithHook();
CoffeeWithHook coffeeWithHook = new CoffeeWithHook();
System.out.println("\n making tea....");
teaWithHook.prepareRecipe();
System.out.println("\n making coffee....");
coffeeWithHook.prepareRecipe();
}
}
2. 优缺点
| 标题 | 内容 |
|---|---|
| 优点 | 1. 对算法有很多的控制权。2. 没有重复代码。将重复使用的代码放到超类中。3. 算法的结构固定。4. 使用继承实现算法,需要的对象少,并且实现简单。 |
| 缺点 | 1. 使用继承实现算法,与策略模式相比,弹性不足且类依赖程度高。 |
3. 解惑
1. 当我创建一个模板方法时,怎么才能知道什么时候该使用抽象方法,什么时候使用钩子呢?
当你的子类必须提供算法中某个方法或步骤的实现时,就使用抽象方法。如果算法的这个部分是可选的,就用钩子。如果是钩子的话,子类可以选择实现这个钩子,但并不强制这么做。
2. 使用钩子的真正目的是什么?
钩子有几种用法。
- 钩子可以让子类实现算法中可选的部分,或者在钩子对于子类的实现并不重要的时候,子类可以对此钩子置之不理。
- 让子类可以有机会对模板方法中某些即将发生的或已经发生的步骤做出反应。
- 钩子也可以让子类有能力为其抽象类做一些决定。
3. 似乎应该保持超类中的抽象方法越少越好,否则,在子类中实现这些方法将会很麻烦
当你在写模板方法的时候,可以让算法内的步骤不要切割的太细。但是如果步骤太少的话,会比较没有弹性。所以要看情况折中。也请记住,有些步骤是可选的,所以你可以将这些步骤实现成钩子,而不是实现成抽象方法,这样就可以让子类的负荷减轻。
二、要点总结
- "模板方法"定义了算法的步骤,把这些步骤的实现延迟到子类。
- 模板方法模式为我们提供了一种代码复用的重要技巧。
- 模板方法的抽象类可以定义具体方法、抽象方法和钩子。
- 抽象方法由子类实现。
- 钩子是一种方法,它在抽象类中不做事,或者只做默认的事情,子类可以选择要不要去覆盖它。
- 为了防止子类改变模板方法中的算法,可以将模板方法声明为final。
- 好莱坞原则告诉我们,将决策权放到高层模块中,以便决定如何以及何时调用低层模块。
- 你将在真实世界代码中看到模板方法模式的许多变体,不要期待它们全都是一眼就可以被你认出的。
- 策略模式和模板方法模式都封装算法,一个用组合,一个用继承。
- 工厂方法是模板方法的一个特殊版本。
三、使用场景
1. 在Java API中,使用模板方法模式的例子
- java.io的InputStream类有一个read()方法,是由子类实现的,而这个方法又会被read(byte b[], int off, int len)模板方法使用。
2. 其他使用场景
- 在生成不同类型的报告时,如HTML报告、PDF报告或Excel报告,虽然生成报告的基本步骤(如数据收集、格式化、输出)是相似的,但每种格式的具体实现细节(如HTML标签的使用、PDF文档结构的设置等)会有所不同。
csharp
public abstract class Report {
public final void generateReport() {
prepareData();
writeReport();
formatReport();
outputReport();
}
protected abstract void prepareData();
protected abstract void writeReport();
protected void formatReport() { /* 默认实现 */ }
protected abstract void outputReport();
}
public class PDFReport extends Report {
@Override
protected void prepareData() { /* PDF数据准备 */ }
@Override
protected void writeReport() { /* 写入PDF */ }
@Override
protected void outputReport() { /* 输出PDF */ }
}
- 发送不同类型的邮件,如普通文本邮件、HTML邮件或带附件的邮件,虽然发送邮件的基本步骤(如连接邮件服务器、设置邮件内容、发送邮件)是相似的,但每种类型的具体实现细节(如HTML内容的构建、附件的处理)会有所不同。
typescript
public abstract class EmailSender {
public final void sendEmail(String to, String subject) {
connectToServer();
setEmailContent(to, subject);
send();
disconnectFromServer();
}
protected abstract void connectToServer();
protected abstract void setEmailContent(String to, String subject);
protected abstract void send();
protected abstract void disconnectFromServer();
}
public class HTMLMailSender extends EmailSender {
@Override
protected void connectToServer() { /* 连接服务器 */ }
@Override
protected void setEmailContent(String to, String subject) { /* 设置HTML内容 */ }
@Override
protected void send() { /* 发送HTML邮件 */ }
@Override
protected void disconnectFromServer() { /* 断开连接 */ }
}