文章目录
- 一、概述
-
- [1.1 结构与角色](#1.1 结构与角色)
- [1.2 模板方法中的方法分类](#1.2 模板方法中的方法分类)
- [1.3 适用场景](#1.3 适用场景)
- 二、实现方式
-
- [2.1 抽象模板](#2.1 抽象模板)
- [2.2 钩子方法](#2.2 钩子方法)
-
- [2.2.1 条件控制型钩子](#2.2.1 条件控制型钩子)
- [2.2.2 扩展点型钩子](#2.2.2 扩展点型钩子)
- [三、JDK 源码中的模板方法](#三、JDK 源码中的模板方法)
-
- [3.1 AbstractList------列表操作的模板](#3.1 AbstractList——列表操作的模板)
- [3.2 InputStream------字节输入流的模板](#3.2 InputStream——字节输入流的模板)
- [3.3 HttpServlet------HTTP 请求处理的模板](#3.3 HttpServlet——HTTP 请求处理的模板)
- [3.4 Arrays.sort() 中的模板方法思想](#3.4 Arrays.sort() 中的模板方法思想)
- 四、总结
一、概述
在软件开发中,经常会遇到这样的场景:多个业务流程的整体步骤是固定的,但其中某些步骤的具体实现各不相同。比如:
- 做饭:买菜 → 洗菜 → 烹饪 → 装盘,其中"烹饪"这一步,炒菜是爆炒、煲汤是慢炖、烤肉是烘烤,步骤相同但实现不同
- 银行办理业务:取号 → 排队 → 办理业务 → 评价,其中"办理业务"有存款、取款、转账等不同操作
- 软件开发流程:需求分析 → 设计 → 编码 → 测试 → 部署,其中"编码"可以是 Java、Python、Go 等不同语言实现
如果为每一种具体流程都写一个完整的类,会导致大量重复代码,且当整体步骤发生变化时,所有类都需要修改:
炒菜类
买菜
洗菜
爆炒
装盘
煲汤类
买菜
洗菜
慢炖
装盘
烤肉类
买菜
洗菜
烘烤
装盘
买菜、洗菜、装盘这些步骤在三个类中完全重复,只有"烹饪"这一步不同。
模板方法模式(Template Method Pattern)正是为了解决这个问题而诞生的------它在父类中定义一个算法的骨架(模板方法),将某些步骤推迟到子类中实现,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些特定步骤。
生活中的模板方法例子:
- 简历模板:简历的框架是固定的(个人信息、教育经历、工作经历、技能特长),但每个人的具体内容不同
- 开车:发动 → 踩离合 → 挂挡 → 踩油门 → 松离合,手动挡和自动挡的"挂挡"步骤不同,但整体流程一样
- 考试答题:审题 → 作答 → 检查 → 交卷,不同题型的"作答"方式不同(选择、填空、简答)
核心:在父类中定义算法骨架,将某些步骤延迟到子类中实现,子类可以在不改变算法结构的前提下重新定义某些特定步骤
1.1 结构与角色
模板方法模式包含以下角色:
声明抽象方法
继承
实现抽象方法
Client 客户端
ConcreteClass 具体子类
AbstractClass 抽象模板类
- AbstractClass(抽象模板类):定义了一组基本方法(抽象方法 + 具体方法),并在模板方法中编排这些基本方法的调用顺序,构成算法骨架
- ConcreteClass(具体子类):继承抽象模板类,实现父类中声明的抽象方法,也可以覆盖父类的具体方法(钩子方法)
- Client(客户端):创建具体子类对象,调用模板方法执行完整流程
1.2 模板方法中的方法分类
模板方法模式涉及三类方法:
| 方法类型 | 说明 | 谁来实现 |
|---|---|---|
| 模板方法 | 定义算法骨架,调用基本方法完成流程 | 抽象模板类 |
| 抽象方法 | 算法中可变的步骤,必须由子类实现 | 具体子类 |
| 具体方法 | 算法中固定的步骤,子类可直接继承 | 抽象模板类 |
| 钩子方法 | 在算法骨架中提供默认实现,子类可选择性覆盖 | 抽象模板类(默认) / 具体子类(覆盖) |
关键点 :模板方法用
final关键字修饰,防止子类覆盖算法骨架,确保算法结构不被破坏。
1.3 适用场景
- 多个类有相同的行为,且整体步骤固定,但某些步骤的实现不同
- 需要统一控制算法的执行流程,同时允许子类自定义部分步骤
- 需要扩展或定制算法中的某些特定步骤,而不影响整体结构
- 重构时发现多个子类有公共行为,可以将其提取到父类中
二、实现方式
2.1 抽象模板
以"制作饮品"为例,泡茶和泡咖啡的整体流程相同:烧水 → 冲泡 → 倒入杯中 → 加调料,但冲泡和加调料的步骤不同:
具体子类 Coffee
具体子类 Tea
抽象模板类 CaffeineBeverage
实现
实现
实现
实现
templateMethod: 算法骨架
boilWater: 烧水
brew: 冲泡-抽象方法
pourInCup: 倒入杯中
addCondiments: 加调料-抽象方法
brew: 用热水浸泡茶叶
addCondiments: 加柠檬
brew: 用热水冲泡咖啡粉
addCondiments: 加糖和牛奶
(1)抽象模板类
java
/**
* 抽象模板类:含咖啡因饮品
* 定义了制作饮品的算法骨架,将可变步骤延迟到子类实现
*/
public abstract class CaffeineBeverage {
/**
* 模板方法:定义算法骨架
* 用 final 修饰,防止子类覆盖算法结构
*/
public final void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
/**
* 具体方法:烧水(固定步骤,所有子类共用)
*/
private void boilWater() {
System.out.println("把水煮沸");
}
/**
* 抽象方法:冲泡(可变步骤,由子类实现)
*/
protected abstract void brew();
/**
* 具体方法:倒入杯中(固定步骤,所有子类共用)
*/
private void pourInCup() {
System.out.println("倒入杯中");
}
/**
* 抽象方法:加调料(可变步骤,由子类实现)
*/
protected abstract void addCondiments();
}
(2)具体子类------茶
java
/**
* 具体子类:茶
*/
public class Tea extends CaffeineBeverage {
@Override
protected void brew() {
System.out.println("用热水浸泡茶叶");
}
@Override
protected void addCondiments() {
System.out.println("加柠檬");
}
}
(3)具体子类------咖啡
java
/**
* 具体子类:咖啡
*/
public class Coffee extends CaffeineBeverage {
@Override
protected void brew() {
System.out.println("用热水冲泡咖啡粉");
}
@Override
protected void addCondiments() {
System.out.println("加糖和牛奶");
}
}
(4)客户端调用
java
public class TemplateMethodDemo {
public static void main(String[] args) {
// 制作茶
CaffeineBeverage tea = new Tea();
tea.prepareRecipe();
// 把水煮沸
// 用热水浸泡茶叶
// 倒入杯中
// 加柠檬
System.out.println("---");
// 制作咖啡
CaffeineBeverage coffee = new Coffee();
coffee.prepareRecipe();
// 把水煮沸
// 用热水冲泡咖啡粉
// 倒入杯中
// 加糖和牛奶
}
}
关键点 :客户端只调用
prepareRecipe()模板方法,算法的执行流程由父类统一控制。新增一种饮品只需新增一个子类并实现两个抽象方法,无需修改父类和已有的子类,符合开闭原则。
2.2 钩子方法
钩子方法(Hook Method)是模板方法模式中的一种特殊方法------它在抽象模板类中提供了默认实现 (通常是空实现),子类可以选择性覆盖,用于控制算法流程或扩展功能。
钩子方法有两种常见用法:
| 用法 | 说明 | 示例 |
|---|---|---|
| 条件控制 | 在模板方法中通过钩子方法的返回值决定是否执行某个步骤 | customerWantsCondiments() 控制是否加调料 |
| 扩展点 | 提供一个空实现的钩子方法,子类按需覆盖以插入自定义逻辑 | beforeBrew() / afterBrew() 前后置钩子 |
2.2.1 条件控制型钩子
以"制作饮品"为例,有些人喝茶不加任何调料,我们可以通过钩子方法来控制是否执行"加调料"这一步:
java
/**
* 抽象模板类:含咖啡因饮品(带钩子方法)
*/
public abstract class CaffeineBeverageWithHook {
/**
* 模板方法:定义算法骨架
* 通过钩子方法控制"加调料"步骤是否执行
*/
public final void prepareRecipe() {
boilWater();
brew();
pourInCup();
if (customerWantsCondiments()) {
addCondiments();
}
}
private void boilWater() {
System.out.println("把水煮沸");
}
protected abstract void brew();
private void pourInCup() {
System.out.println("倒入杯中");
}
protected abstract void addCondiments();
/**
* 钩子方法:客户是否需要加调料
* 默认返回 true,子类可以覆盖此方法来改变行为
*
* @return true 表示需要加调料,false 表示不加
*/
protected boolean customerWantsCondiments() {
return true;
}
}
(2)具体子类------茶(不加调料)
java
/**
* 具体子类:茶(不加调料)
* 覆盖钩子方法,返回 false,跳过"加调料"步骤
*/
public class TeaWithHook extends CaffeineBeverageWithHook {
@Override
protected void brew() {
System.out.println("用热水浸泡茶叶");
}
@Override
protected void addCondiments() {
System.out.println("加柠檬");
}
/**
* 覆盖钩子方法:茶不加调料
*/
@Override
protected boolean customerWantsCondiments() {
return false;
}
}
(3)具体子类------咖啡(加调料)
java
/**
* 具体子类:咖啡(加调料)
* 不覆盖钩子方法,使用默认值 true
*/
public class CoffeeWithHook extends CaffeineBeverageWithHook {
@Override
protected void brew() {
System.out.println("用热水冲泡咖啡粉");
}
@Override
protected void addCondiments() {
System.out.println("加糖和牛奶");
}
}
(4)客户端调用
java
public class HookDemo {
public static void main(String[] args) {
// 制作茶(不加调料)
CaffeineBeverageWithHook tea = new TeaWithHook();
tea.prepareRecipe();
// 把水煮沸
// 用热水浸泡茶叶
// 倒入杯中
// (没有加调料,因为 customerWantsCondiments() 返回 false)
System.out.println("---");
// 制作咖啡(加调料)
CaffeineBeverageWithHook coffee = new CoffeeWithHook();
coffee.prepareRecipe();
// 把水煮沸
// 用热水冲泡咖啡粉
// 倒入杯中
// 加糖和牛奶
}
}
关键点 :钩子方法
customerWantsCondiments()提供了默认实现(返回true),子类可以按需覆盖。模板方法中通过if (customerWantsCondiments())判断是否执行"加调料"步骤,实现了灵活的流程控制。
2.2.2 扩展点型钩子
钩子方法还可以作为扩展点,让子类在不改变算法骨架的前提下插入自定义逻辑。这种用法类似于 Spring 框架中的 InitializingBean.afterPropertiesSet() 或 @PostConstruct。
以"数据库操作模板"为例,提供 beforeExecute() 和 afterExecute() 两个钩子方法,让子类可以在执行前后插入自定义逻辑(如日志、监控、事务等):
java
/**
* 抽象模板类:数据库操作模板
* 提供前后置钩子方法,子类可按需扩展
*/
public abstract class DatabaseOperationTemplate {
/**
* 模板方法:定义数据库操作的算法骨架
*/
public final void execute() {
beforeExecute();
openConnection();
doOperation();
closeConnection();
afterExecute();
}
private void openConnection() {
System.out.println("打开数据库连接");
}
/**
* 抽象方法:具体的数据库操作
*/
protected abstract void doOperation();
private void closeConnection() {
System.out.println("关闭数据库连接");
}
/**
* 钩子方法:操作前扩展点(默认空实现)
* 子类可覆盖,如记录开始时间、开启事务等
*/
protected void beforeExecute() {
// 默认空实现
}
/**
* 钩子方法:操作后扩展点(默认空实现)
* 子类可覆盖,如记录结束时间、提交事务等
*/
protected void afterExecute() {
// 默认空实现
}
}
(2)具体子类------带日志的查询操作
java
/**
* 具体子类:带日志的查询操作
* 利用钩子方法在操作前后记录耗时
*/
public class QueryWithLog extends DatabaseOperationTemplate {
private long startTime;
@Override
protected void doOperation() {
System.out.println("执行 SELECT 查询");
}
@Override
protected void beforeExecute() {
startTime = System.currentTimeMillis();
System.out.println("[LOG] 查询开始");
}
@Override
protected void afterExecute() {
long cost = System.currentTimeMillis() - startTime;
System.out.println("[LOG] 查询完成,耗时:" + cost + "ms");
}
}
(3)具体子类------简单的更新操作
java
/**
* 具体子类:简单的更新操作
* 不覆盖钩子方法,只实现核心操作
*/
public class SimpleUpdate extends DatabaseOperationTemplate {
@Override
protected void doOperation() {
System.out.println("执行 UPDATE 更新");
}
}
(4)客户端调用
java
public class DatabaseDemo {
public static void main(String[] args) {
// 带日志的查询
DatabaseOperationTemplate query = new QueryWithLog();
query.execute();
// [LOG] 查询开始
// 打开数据库连接
// 执行 SELECT 查询
// 关闭数据库连接
// [LOG] 查询完成,耗时:0ms
System.out.println("---");
// 简单更新(无日志)
DatabaseOperationTemplate update = new SimpleUpdate();
update.execute();
// 打开数据库连接
// 执行 UPDATE 更新
// 关闭数据库连接
}
}
关键点 :扩展点型钩子方法提供空实现,子类按需覆盖。查询操作利用钩子记录日志,更新操作不覆盖钩子、保持简洁。这种设计让算法骨架保持稳定,同时提供了足够的扩展性。
三、JDK 源码中的模板方法
模板方法模式在 JDK 中有着广泛的应用,下面来看几个经典的例子。
3.1 AbstractList------列表操作的模板
java.util.AbstractList 是模板方法模式的典型代表。它实现了 List<E> 接口,在 get(int index)、set(int index, E element)、add(int index, E element)、remove(int index) 等方法中提供了基于随机访问的默认实现,而将 get(int index) 和 size() 声明为抽象方法,由子类实现:
java
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
/**
* 抽象方法:获取指定位置的元素(子类必须实现)
*/
public abstract E get(int index);
/**
* 抽象方法:返回列表大小(子类必须实现)
*/
public abstract int size();
/**
* 模板方法中的可覆盖步骤:设置指定位置的元素
* 默认抛出 UnsupportedOperationException,子类可按需覆盖
*/
public E set(int index, E element) {
throw new UnsupportedOperationException();
}
/**
* 模板方法中的可覆盖步骤:在指定位置插入元素
* 默认抛出 UnsupportedOperationException,子类可按需覆盖
*/
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
/**
* 模板方法中的可覆盖步骤:移除指定位置的元素
* 默认抛出 UnsupportedOperationException,子类可按需覆盖
*/
public E remove(int index) {
throw new UnsupportedOperationException();
}
}
JDK 中的继承体系:
List 接口
AbstractList 抽象模板类
ArrayList 具体子类
Vector 具体子类
AbstractSequentialList 中间抽象类
LinkedList 具体子类
在这个例子中:
- AbstractClass(抽象模板类) :
AbstractList,定义了get()和size()为抽象方法,set()、add()、remove()为可覆盖步骤(抛出异常作为默认实现,类似于钩子方法) - ConcreteClass(具体子类) :
ArrayList实现了基于数组的get(),LinkedList通过AbstractSequentialList实现了基于链表的get()
关键点 :
AbstractList中set()、add()、remove()的默认实现抛出UnsupportedOperationException,这就是一种钩子方法的变体------默认行为是"不支持",子类按需覆盖以提供实际功能。
3.2 InputStream------字节输入流的模板
java.io.InputStream 是另一个经典案例。它定义了 read() 方法的骨架,子类只需实现读取单个字节的 read() 方法:
java
public abstract class InputStream implements Closeable {
/**
* 抽象方法:读取数据的下一个字节(子类必须实现)
*/
public abstract int read() throws IOException;
/**
* 模板方法:读取多个字节到字节数组中
* 内部循环调用抽象方法 read() 逐个读取
*/
public int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
int c = read(); // 调用抽象方法
if (c == -1) {
return -1;
}
b[off] = (byte) c;
int i = 1;
for (; i < len; i++) {
c = read(); // 调用抽象方法
if (c == -1) {
break;
}
b[off + i] = (byte) c;
}
return i;
}
/**
* 钩子方法:可跳过的字节数
* 默认返回 0,子类可覆盖
*/
public long skip(long n) throws IOException {
// 默认实现...
return 0;
}
}
JDK 中的继承体系:
InputStream 抽象模板类
FileInputStream
ByteArrayInputStream
BufferedInputStream
ObjectInputStream
在这个例子中:
- AbstractClass(抽象模板类) :
InputStream,定义了read()为抽象方法 - ConcreteClass(具体子类) :
FileInputStream(从文件读取)、ByteArrayInputStream(从字节数组读取)等,各自实现read()方法
关键点 :
InputStream.read(byte[], int, int)就是模板方法,它编排了算法流程------循环调用抽象方法read()逐字节读取,子类只需关心如何读取单个字节,不用操心缓冲区、边界检查等通用逻辑。
3.3 HttpServlet------HTTP 请求处理的模板
在 Java Web 开发中,javax.servlet.http.HttpServlet 也是模板方法模式的经典应用。它的 service() 方法根据 HTTP 请求方法分发给对应的 doXxx() 方法:
java
public abstract class HttpServlet extends GenericServlet {
/**
* 模板方法:根据请求方法分发到对应的 doXxx 方法
*/
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String method = req.getMethod();
if ("GET".equals(method)) {
doGet(req, resp);
} else if ("POST".equals(method)) {
doPost(req, resp);
} else if ("PUT".equals(method)) {
doPut(req, resp);
} else if ("DELETE".equals(method)) {
doDelete(req, resp);
} else if ("HEAD".equals(method)) {
doHead(req, resp);
} else if ("OPTIONS".equals(method)) {
doOptions(req, resp);
} else if ("TRACE".equals(method)) {
doTrace(req, resp);
} else {
// 不支持的请求方法
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
}
}
/**
* 钩子方法:处理 GET 请求
* 默认返回 405 错误,子类按需覆盖
*/
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
}
/**
* 钩子方法:处理 POST 请求
* 默认返回 405 错误,子类按需覆盖
*/
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
}
// doPut、doDelete、doHead、doOptions、doTrace 类似...
}
使用方式:
java
/**
* 自定义 Servlet:覆盖 doGet 和 doPost 钩子方法
*/
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write("<h1>处理 GET 请求</h1>");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String name = req.getParameter("name");
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write("<h1>处理 POST 请求,name=" + name + "</h1>");
}
}
在这个例子中:
- AbstractClass(抽象模板类) :
HttpServlet,service()是模板方法,doGet()、doPost()等是钩子方法 - ConcreteClass(具体子类) :自定义 Servlet,覆盖需要的
doXxx()钩子方法 - 钩子方法的特点:默认返回 405 错误,子类只需覆盖自己支持的 HTTP 方法,不需要的保持默认即可
关键点 :
HttpServlet.service()是模板方法,它定义了 HTTP 请求分发的算法骨架。doGet()、doPost()等是钩子方法,提供默认实现(返回 405),子类按需覆盖,这就是条件控制型钩子在真实框架中的应用。
3.4 Arrays.sort() 中的模板方法思想
java.util.Arrays 中的排序方法也体现了模板方法的思想------排序算法的骨架是固定的(基于 TimSort / 双轴快排),而元素之间的比较规则由外部传入的 Comparator 决定:
java
import java.util.Arrays;
import java.util.Comparator;
public class ArraysSortTemplateDemo {
public static void main(String[] args) {
String[] names = {"Charlie", "Alice", "Bob", "David"};
// 算法骨架固定,比较策略由 Comparator 决定
Arrays.sort(names, Comparator.comparingInt(String::length));
System.out.println("按长度排序:" + Arrays.toString(names));
// 按长度排序:[Bob, Alice, David, Charlie]
Arrays.sort(names, Comparator.naturalOrder());
System.out.println("按字典序排序:" + Arrays.toString(names));
// 按字典序排序:[Alice, Bob, Charlie, David]
}
}
说明 :严格来说,
Arrays.sort()结合Comparator是策略模式 + 模板方法 的混合应用------排序算法的骨架是模板方法,比较策略由Comparator策略接口决定。
四、总结
模板方法模式的核心思想是在父类中定义算法骨架,将可变步骤延迟到子类中实现,子类可以在不改变算法结构的前提下重新定义某些特定步骤。
优点:
- 代码复用:公共步骤在父类中实现一次,所有子类共享,避免重复代码
- 符合开闭原则:新增具体子类只需实现抽象方法,无需修改父类和已有子类
- 统一控制算法流程:模板方法用
final修饰,确保算法结构不被破坏 - 灵活扩展:钩子方法提供了灵活的扩展点,子类可选择性覆盖
- 反向控制:父类调用子类的方法(而非子类调用父类),实现了"好莱坞原则"------Don't call us, we'll call you
缺点:
- 类数量增加:每种具体实现都需要一个子类,实现越多子类越多
- 子类受限于父类:算法骨架由父类定义,子类只能扩展而不能改变算法结构
- 可读性下降:算法分散在父类和多个子类中,理解完整流程需要跳转多个类
适用场景:
- 多个类有相同的行为,整体步骤固定,但某些步骤的实现不同
- 需要统一控制算法执行流程,同时允许子类自定义部分步骤
- 需要在算法的某些步骤前后插入自定义逻辑(钩子方法)
模板方法模式 vs 策略模式 :模板方法模式通过继承 实现算法骨架的复用,子类覆盖部分步骤来改变行为;策略模式通过组合 实现算法的灵活切换,上下文持有策略接口的引用。模板方法模式侧重于固定算法结构 、变化部分步骤;策略模式侧重于整体算法替换、运行时动态切换。
| 对比维度 | 模板方法模式 | 策略模式 |
|---|---|---|
| 实现方式 | 继承 | 组合 |
| 算法控制 | 父类固定算法骨架 | 策略可整体替换 |
| 扩展方式 | 新增子类 | 新增策略类 |
| 运行时切换 | 不支持(编译时确定子类) | 支持(运行时切换策略对象) |
| 代码复用 | 父类公共代码被所有子类共享 | 策略间无代码共享 |
| 设计原则 | 好莱坞原则 | 开闭原则 + 合成复用原则 |
参考博客:
模板方法模式 | 菜鸟教程:https://www.runoob.com/design-pattern/template-pattern.html