模板方法模式
模板方法模式
Rif设计模式笔记:模板方法模式 (Template Method Pattern)
一、一句话概括
在一个方法中定义一个算法的骨架,而将一些具体的步骤延迟到子类中去实现。这使得子类可以在不改变算法整体结构的情况下,重新定义算法中的某些特定步骤。
二、为什么需要它 (Why - The Pain Point)
想象一下,你正在开发一个泡制饮料的程序。你需要支持泡咖啡(Coffee)和泡茶(Tea)。我们来分析一下它们的制作流程:
泡咖啡(prepareCoffee()):
- 把水烧开 (
boilWater()) - 用沸水冲泡咖啡 (
brewCoffeeGrinds()) - 把咖啡倒进杯子 (
pourInCup()) - 加糖和牛奶 (
addSugarAndMilk())
泡茶(prepareTea()):
- 把水烧开 (
boilWater()) - 用沸水浸泡茶叶 (
steepTeaBag()) - 把茶倒进杯子 (
pourInCup()) - 加柠檬 (
addLemon())
痛点:代码重复和流程僵化
如果你为 Coffee 和 Tea 分别编写代码,你会发现:
- 代码重复:步骤1(烧水)和步骤3(倒进杯子)是完全一样的。代码重复是万恶之源。
- 流程不统一:虽然大体流程相似,但没有一个统一的、强制性的算法框架来约束它们。如果未来新增一种饮料,开发者可能会忘记某个步骤,或者打乱步骤的顺序。
核心问题:如何在一个流程或算法中,既能复用公共的部分,又能方便地定制变化的部分,同时还能保证整个流程的结构稳定不变?
三、它是什么 (What - The Solution)
模板方法模式通过继承来解决这个问题。它定义了一个“模板方法”,这个方法位于抽象父类中,它封装了整个算法的固定结构。
核心思想和角色:
**抽象类 (Abstract Class)**:
- 它包含一个模板方法(Template Method)。这个方法是
public和final的,以防止子类重写它,从而保证算法结构的稳定性。 - 模板方法内部按顺序调用了一系列“步骤方法”。
- 这些步骤方法可以是:
- 抽象方法 (Abstract Method):必须由子类实现。这些是算法中必须变化的部分。例如,
brew()(冲泡原料)。 - 具体方法 (Concrete Method):父类提供默认实现,子类可以选择性地重写。这些是算法中可能变化的部分。
- **钩子 (Hook)**:这是一种特殊的具体方法。它通常在父类中有一个空的实现,或者返回一个默认值。子类可以通过重写钩子来“挂入”算法流程的某个特定点,以影响算法的行为。例如,一个
customerWantsCondiments()(顾客是否要加调料)的钩子,如果子类重写它返回false,模板方法就可以跳过“加调料”这一步。
- 抽象方法 (Abstract Method):必须由子类实现。这些是算法中必须变化的部分。例如,
- 它包含一个模板方法(Template Method)。这个方法是
**具体类 (Concrete Class)**:
- 继承自抽象类。
- 实现了父类中所有的抽象方法。
- 根据需要重写父类中的具体方法或钩子。
- 它不需要(也不能)重写模板方法。
好莱坞原则 (Hollywood Principle): “Don’t call us, we’ll call you.”
模板方法模式完美地体现了这一原则。父类(框架)控制着整个算法的流程,它在需要的时候会“调用”子类(用户代码)实现的具体步骤,而不是让子类来驱动整个流程。这是一种控制反转(Inversion of Control, IoC) 的体现。
四、怎么做 (How - The Blueprint)
基本步骤(以泡饮料为例):
- **创建抽象类
CaffeineBeverage**。 - 在
CaffeineBeverage中,定义一个final的模板方法prepareRecipe()。 - 在
prepareRecipe()中,定义算法的骨架:1
2
3
4
5
6boilWater();
brew(); // 变化的步骤
pourInCup();
if (customerWantsCondiments()) { // 钩子
addCondiments(); // 变化的步骤
} - 将公共步骤
boilWater()和pourInCup()实现为具体方法。 - 将变化的步骤
brew()和addCondiments()声明为抽象方法。 - 将
customerWantsCondiments()实现为一个钩子(默认返回true)。 - 创建具体子类
Coffee和Tea,继承CaffeineBeverage,并实现brew()和addCondiments()方法。Tea还可以重写钩子customerWantsCondiments()来控制是否加柠檬。
Java 示例代码
1 | // 1. 抽象类 (AbstractClass) |
五、UML 类图
1 | @startuml |
六、优缺点分析
优点 (Pros):
- 代码复用:将公共代码提取到父类中,避免了代码重复。
- 封装不变部分,扩展可变部分:算法的骨架(不变部分)被固定在父类,而具体实现(可变部分)则委托给子类,非常符合开闭原则。
- 框架化:这是构建框架的常用技术。框架定义了主要的控制流程,开发者只需继承并填充特定的业务逻辑即可。
- 控制反转:父类调用子类的操作,通过这种反向控制,确保了算法结构的统一性。
缺点 (Cons):
- 继承的限制:模板方法模式是基于继承的,这带来了一些固有的限制。比如子类必须继承父类,这在某些情况下可能不方便。
- 可读性可能降低:如果父类的模板方法过于复杂,或者钩子方法过多,可能会让子类的实现者难以理解算法的完整流程。
七、在 Java 中的应用
模板方法模式在 Java JDK 和各种框架中被广泛使用。
java.io.InputStream:read(byte[] b)方法内部调用了抽象的read()方法,这是一个典型的模板方法。子类如FileInputStream只需要实现基本的read(),更高效的read(byte[] b)的逻辑已经由父类提供了。java.util.AbstractList: 它是List接口的一个抽象实现。它实现了add,remove等方法,但这些方法内部依赖于像get(int index),set(int index, E element)这样的抽象方法。如果你想创建一个自己的List实现,只需继承AbstractList并实现这些核心的抽象方法,就能免费获得大部分List功能。- Java Servlet 框架:
javax.servlet.http.HttpServlet类中的service()方法就是一个模板方法。它根据 HTTP 请求的类型(GET, POST 等)来调用具体的doGet(),doPost()等方法。开发者只需继承HttpServlet并重写doGet()或doPost(),而无需关心service()方法中复杂的请求分派逻辑。 - Spring 框架:
JdbcTemplate,RestTemplate等模板类中大量使用了该模式(通常与回调函数结合),来封装资源管理、异常处理等固定流程。