行为型模式:观察者、中介者与模板方法
对象之间的「沟通艺术」
前几节我们已经见过两种行为型模式——状态模式让对象根据内部状态切换行为,命令模式将请求封装为对象以实现发送者与接收者的解耦。它们都聚焦于单个对象如何处理行为变化。
但真实的系统远不止一个对象在独舞。当多个对象需要协作时,新的问题浮现了:一个对象的状态变化了,谁需要知道?多个对象之间你来我往,怎么避免「谁都认识谁」的混乱?一段算法的骨架是固定的,但各步骤的实现因场景而异,怎么复用这个骨架?
本节介绍的三种行为型模式,分别回答了这三个问题:
- 观察者模式——一对多通知:一个对象变化,自动通知所有关注者
- 中介者模式——多对多协调:引入「中间人」,将网状依赖变成星形
- 模板方法模式——算法骨架复用:父类定义流程,子类填充细节
观察者模式
订阅与通知
每天早晨,邮递员把报纸投递到订阅者的信箱里。报社不需要知道每个订阅者是谁——它只负责印报纸和通知投递系统。订阅者也不必每天跑到报社去问「今天有没有新报纸」——报纸会自动送到。当有人不想看了,退订即可,报社照常运转。
这个场景完美诠释了观察者模式的核心思想:当「被观察的对象」发生变化时,所有「观察者」自动收到通知并更新自己。观察者可以随时订阅或取消订阅,发布者不受影响。
模式定义
观察者模式
观察者模式(Observer Pattern)定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。
Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
别名众多:发布-订阅模式(Publish/Subscribe)、模型-视图模式(Model/View)、源-监听器模式(Source/Listener)、从属者模式(Dependents)。属于对象行为型模式。
「一对多」是关键词——一个目标对象对应多个观察者,且观察者之间互不关联。目标只知道「我有一群观察者」,不关心它们具体是谁、有多少个。
模式结构
classDiagram
direction LR
class Subject {
<<abstract>>
-observers : List~Observer~
+attach(Observer obs) void
+detach(Observer obs) void
+notifyObservers() void
}
class ConcreteSubject {
-subjectState : Object
+getState() Object
+setState(Object state) void
}
class Observer {
<<interface>>
+update()* void
}
class ConcreteObserver {
-observerState : Object
+update() void
}
Subject <|-- ConcreteSubject
Observer <|.. ConcreteObserver
Subject o--> Observer : observers
ConcreteObserver ..> ConcreteSubject : observes
四个角色:
| 角色 | 职责 |
|---|---|
| Subject(目标) | 维护观察者列表,提供注册、注销和通知方法 |
| ConcreteSubject(具体目标) | 存储具体状态,状态变化时触发通知 |
| Observer(观察者) | 定义更新接口 |
| ConcreteObserver(具体观察者) | 实现更新逻辑,保持自身状态与目标同步 |
工作机制
整个模式的运作可以用一句话概括:Subject 维护一个观察者列表,状态变化时遍历列表逐一通知。
抽象目标类管理观察者的注册与通知:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public abstract class Subject { protected List<Observer> observers = new ArrayList<>(); public void attach(Observer observer) { observers.add(observer); } public void detach(Observer observer) { observers.remove(observer); } public void notifyObservers() { for (Observer obs : observers) { obs.update(); // 逐一通知 } } } |
具体目标类在状态变化时调用 notifyObservers():
1 2 3 4 5 6 7 8 9 10 | public class ConcreteSubject extends Subject { private String state; public String getState() { return state; } public void setState(String state) { this.state = state; notifyObservers(); // 状态一变,立即通知 } } |
观察者接口只需要一个 update() 方法:
1 2 3 | public interface Observer { void update(); } |
具体观察者在 update() 中获取目标的最新状态并据此更新自身:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class ConcreteObserver implements Observer { private ConcreteSubject subject; // 持有目标引用,以便获取状态 public ConcreteObserver(ConcreteSubject subject) { this.subject = subject; subject.attach(this); // 注册为观察者 } @Override public void update() { String newState = subject.getState(); // 根据 newState 更新自身... } } |
客户端的使用非常简洁:
1 2 3 4 | ConcreteSubject subject = new ConcreteSubject(); Observer obsA = new ConcreteObserver(subject); // 注册观察者 A Observer obsB = new ConcreteObserver(subject); // 注册观察者 B subject.setState("new state"); // 触发通知 → obsA.update() + obsB.update() |
注意,具体观察者持有对具体目标的引用——这是为了在收到通知后能主动「拉取」目标的最新状态。根据通知方式的不同,观察者模式有两种变体:
| 变体 | 通知方式 | 特点 |
|---|---|---|
| 拉模型(Pull Model) | 目标只告诉观察者「我变了」,观察者自行查询变化细节 | 耦合度更低,但粒度较粗 |
| 推模型(Push Model) | 目标在通知时直接把变化数据作为参数传给 update() |
效率更高,但要求目标了解观察者需要什么数据 |
上面的代码采用的是拉模型——update() 无参数,观察者在收到通知后通过 getState() 主动拉取。
下面的时序图展示了一次完整的状态变更与通知流程:
sequenceDiagram
participant Client
participant Subject as ConcreteSubject
participant ObsA as Observer A
participant ObsB as Observer B
Client->>Subject: attach(obsA)
Client->>Subject: attach(obsB)
Client->>Subject: setState("new state")
activate Subject
Subject->>ObsA: update()
ObsA->>Subject: getState()
Subject-->>ObsA: "new state"
Subject->>ObsB: update()
ObsB->>Subject: getState()
Subject-->>ObsB: "new state"
deactivate Subject
实例:猫、狗与老鼠
来看一个生动的小例子。假设猫是老鼠和狗的观察目标——猫一叫,老鼠跑,狗也跟着叫。
classDiagram
class Cat {
-name : String
+cry() void
}
class MyObserver {
<<interface>>
+response()* void
}
class Mouse {
-name : String
+response() void
}
class Dog {
-name : String
+response() void
}
Cat --> MyObserver : observers
MyObserver <|.. Mouse
MyObserver <|.. Dog
Cat 在 cry() 方法中调用 notifyObservers(),Mouse 和 Dog 分别在 response() 中输出「老鼠跑了」和「狗也叫了」。这个例子虽然简单,但清晰地展示了观察者模式的核心:目标对象不需要知道观察者的具体类型——猫不知道被它吓到的是老鼠还是狗,它只管叫。后续如果要添加新的观察者(比如主人被吵醒),只需实现 MyObserver 接口并注册到 Cat 上,Cat 类不需要任何修改。
优缺点
优点:
| 优点 | 说明 |
|---|---|
| 表示层与逻辑层分离 | 观察者(View)与目标(Model)各自独立变化 |
| 抽象耦合 | 目标只依赖 Observer 接口,不依赖具体观察者 |
| 广播通信 | 一次状态变化通知所有观察者 |
| 符合开闭原则 | 新增观察者无需修改目标类 |
缺点:
- 观察者很多时,逐一通知耗时较长,可能影响性能
- 若观察者与目标之间存在循环依赖,通知链可能导致死循环甚至系统崩溃
- 观察者只知道「目标变了」,但不知道具体怎么变的——缺乏细粒度的变化描述
第二个缺点值得警惕。如果 A 观察 B,B 又观察 A,状态变化会在两者之间无限弹跳。设计时应确保观察关系是单向的,或者在通知过程中加入防重入机制。
适用场景
- 一个抽象模型有两个方面,其中一方面依赖于另一方面——封装在独立对象中使它们可以各自独立变化和复用
- 一个对象的改变需要同时改变其他对象,但不知道具体有多少对象需要改变
- 一个对象必须通知其他对象,而不能假设这些对象是谁
- 需要建立触发链:A 的行为影响 B,B 影响 C……
在实际开发中,观察者模式的应用无处不在:电商平台在商品降价后向所有关注该商品的用户推送通知,团队游戏中某队友阵亡时向所有队员弹出提示——凡是涉及一对多的对象交互场景,都可以考虑观察者模式。
Java 事件处理与 MVC
观察者模式在实际系统中的应用远比课堂例子广泛。最典型的两个场景:
Java 委派事件模型。从 JDK 1.1 开始,Java 的 GUI 事件处理就采用了基于观察者模式的委派事件模型(Delegation Event Model, DEM)。在这个模型中:
- 事件源(Event Source)= Subject,如 Button、TextField 等 Swing/AWT 组件
- 事件监听器(Event Listener)= Observer,如
ActionListener、MouseListener - 事件对象(Event Object)= 携带变化信息的数据载体
当用户点击按钮时,按钮(事件源)创建一个 ActionEvent 对象,然后通知所有注册的 ActionListener——这正是 Subject 调用 notifyObservers() 的过程。除了 AWT/Swing,Java 的 SAX2 XML 解析和 Servlet 技术中的事件处理机制也基于 DEM。
基于这一机制,开发者可以自定义 Java 控件并为其添加事件支持。例如自定义一个登录控件(LoginBean),定义 LoginEvent 和 LoginEventListener,当用户点击登录按钮时触发事件通知——底层的订阅-通知机制完全由 Swing/AWT 框架封装。
JDK 曾在 java.util 包中提供 Observable 类和 Observer 接口,作为观察者模式的内置支持。但由于 Observable 是一个类(而非接口),不支持多继承,使用限制较大,已在 Java 9 中被标记为废弃。现代 Java 开发中更推荐使用 PropertyChangeListener 或响应式框架来实现观察者模式。
MVC 架构模式。MVC(Model-View-Controller)是观察者模式最经典的架构级应用:
flowchart LR
M[Model<br/>数据模型] -- "通知变化" --> V[View<br/>视图展示]
V -- "用户操作" --> C[Controller<br/>控制器]
C -- "更新数据" --> M
classDef model fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
classDef view fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
classDef ctrl fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
class M model
class V view
class C ctrl
Model 是观察目标,View 是观察者。当 Model 的数据发生变化时,View 自动更新显示。Controller 充当两者之间的中介,负责接收用户输入并更新 Model。这种分离使得同一份数据可以有多种展示形式——比如表格视图和图表视图同时观察同一个数据模型——而修改展示方式不影响底层数据。
MVC 中的 Controller 实际上也扮演了中介者的角色——这一点我们在下文的中介者模式中会再次提到。
从观察者到响应式编程
观察者模式的思想在现代编程中演化出了一个更强大的范式:响应式编程(Reactive Programming)。
传统的观察者模式是「有事通知我」,响应式编程则更进一步——它以数据流和变化传播为核心,用声明式的方式描述数据之间的依赖关系。你不再手动管理观察者的注册和注销,而是通过操作符(map、filter、merge……)对数据流进行变换和组合,框架自动处理订阅和通知。它的优势包括:
- 统一的数据流抽象
- 丰富的操作符与函数式风格
- 天然的异步与并发支持
- 统一的错误处理机制
下面是一个 RxCpp 的例子——创建一个 1 到 5 的数据流,每个元素乘以 2,然后订阅输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | int main() { auto observable = rxcpp::observable<>::range(1, 5); auto transformed = observable.map([](int x) { return x * 2; }); transformed.subscribe( [](int x) { std::cout << "on_next: " << x << std::endl; }, []() { std::cout << "on_completed" << std::endl; } ); // 输出:on_next: 2, on_next: 4, on_next: 6, on_next: 8, on_next: 10, on_completed return 0; } |
Java 的 RxJava、JavaScript 的 RxJS、C++ 的 RxCpp 都是这一范式的实现。可以说,响应式编程是观察者模式在数据流时代的「终极进化」。
中介者模式
从混乱到秩序
观察者模式解决了一对多的通知问题。但当系统中的对象之间存在多对多的复杂交互时,情况就不同了。
想象一个没有塔台的机场。每架飞机都需要直接与其他所有飞机通信,协调跑道使用、起降顺序、空中间距……如果有 架飞机,通信链路就有 条。每新增一架飞机,所有在场飞机的通信模块都要更新——这显然不现实。
现实中的解决方案是引入塔台:所有飞机只与塔台通信,由塔台统一协调。通信链路从 降为 ,新增飞机只需让塔台知道即可。
flowchart LR
subgraph before["网状结构 O(n²)"]
direction TB
A1((A)) <--> B1((B))
A1 <--> C1((C))
A1 <--> D1((D))
B1 <--> C1
B1 <--> D1
C1 <--> D1
end
subgraph after["星形结构 O(n)"]
direction TB
M((中介者))
A2((A)) <--> M
B2((B)) <--> M
C2((C)) <--> M
D2((D)) <--> M
end
classDef node fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
classDef mediator fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
class A1,B1,C1,D1,A2,B2,C2,D2 node
class M mediator
软件系统中也是同样的道理。当用户与用户之间直接通信时,对象之间存在大量相互引用,会导致三个问题:
- 系统结构复杂——对象之间的关联和调用关系难以追踪和理解
- 对象可重用性差——一个对象和太多其他对象强耦合,脱离这些对象就无法独立使用
- 系统扩展性低——新增对象需要修改所有与之相关的已有对象
这就是中介者模式要解决的问题:用一个中介对象来封装一系列对象之间的交互,将网状依赖变成以中介者为中心的星形结构。
模式定义
中介者模式
中介者模式(Mediator Pattern)用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.
别名:调停者模式。属于对象行为型模式。
模式结构
classDiagram
class Mediator {
<<abstract>>
+operation()* void
}
class ConcreteMediator {
-colleagues : List~Colleague~
+register(Colleague c) void
+operation() void
}
class Colleague {
<<abstract>>
#mediator : Mediator
+method1()* void
+method2()* void
}
class ConcreteColleagueA {
+method1() void
+method2() void
}
class ConcreteColleagueB {
+method1() void
+method2() void
}
Mediator <|-- ConcreteMediator
Colleague <|-- ConcreteColleagueA
Colleague <|-- ConcreteColleagueB
Colleague --> Mediator : mediator
ConcreteMediator --> Colleague : colleagues
在中介者模式中,与中介者交互的各个对象地位平等,被称为同事(Colleague)——它们彼此之间不直接沟通,所有通信都通过中介者转发。四个角色:
| 角色 | 职责 |
|---|---|
| Mediator(抽象中介者) | 定义与同事对象通信的接口 |
| ConcreteMediator(具体中介者) | 了解并维护各同事对象的引用,协调它们之间的交互 |
| Colleague(抽象同事类) | 定义同事的公共接口,持有中介者引用 |
| ConcreteColleague(具体同事类) | 与其他同事通信时通过中介者转发 |
中转与协调
中介者在系统中承担两方面的职责:
中转作用(结构性)——同事对象之间不再直接引用,所有通信都经过中介者转发。同事 A 想和同事 B 说话,不是直接找 B,而是告诉中介者,由中介者传达给 B。这在结构层面消除了同事之间的直接依赖。
协调作用(行为性)——中介者不仅机械地转发消息,还封装了交互逻辑。同事对象只需说「我要做这件事」,中介者根据自身内部的协调逻辑决定通知哪些同事、以什么顺序、触发什么反应。
典型代码
同事类持有中介者引用,需要与其他同事通信时委托给中介者:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public abstract class Colleague { protected Mediator mediator; public Colleague(Mediator mediator) { this.mediator = mediator; } // 自身的业务方法 public abstract void method1(); // 需要与其他同事交互时,委托给中介者 public void method2() { mediator.operation(); // "我有话说,请转达" } } |
具体同事类实现自身业务逻辑:
1 2 3 4 5 6 7 8 9 10 | public class ConcreteColleagueA extends Colleague { public ConcreteColleagueA(Mediator mediator) { super(mediator); } @Override public void method1() { // 自身业务逻辑 } } |
中介者知道所有同事,并实现协调逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class ConcreteMediator extends Mediator { private List<Colleague> colleagues = new ArrayList<>(); public void register(Colleague colleague) { colleagues.add(colleague); } @Override public void operation() { // 协调逻辑:决定调用哪些同事的哪些方法 // (简化示例——实际中通常用标识符或 Map 分发,避免强制类型转换) ((ConcreteColleagueA) colleagues.get(0)).method1(); // ...其他协调操作 } } |
对比不使用中介者的情况——每个同事都需要持有其他所有同事的引用,新增一个同事就要修改所有已有同事类。而使用中介者后,同事类只认识中介者,不认识其他同事。新增同事只需向中介者注册,已有同事类完全不受影响。
实例:虚拟聊天室
某论坛要增加一个虚拟聊天室功能。普通会员(CommonMember)只能发送文本信息,钻石会员(DiamondMember)既能发送文本信息也能发送图片。聊天室还需要对不雅字符(如「日」等)进行过滤,并对发送的图片大小进行控制。
classDiagram
class AbstractChatRoom {
<<abstract>>
+register(Member m)* void
+sendText(String from, String to, String msg)* void
+sendImage(String from, String to, String img)* void
}
class ChatRoom {
-members : Map~String, Member~
+register(Member m) void
+sendText(String from, String to, String msg) void
+sendImage(String from, String to, String img) void
}
class Member {
<<abstract>>
#name : String
#chatRoom : AbstractChatRoom
+sendText(String to, String msg)* void
+receiveText(String from, String msg) void
}
class CommonMember {
+sendText(String to, String msg) void
}
class DiamondMember {
+sendText(String to, String msg) void
+sendImage(String to, String img) void
}
AbstractChatRoom <|-- ChatRoom
Member <|-- CommonMember
Member <|-- DiamondMember
Member --> AbstractChatRoom : chatRoom
ChatRoom --> Member : members
聊天室(ChatRoom)就是中介者,会员就是同事。会员发消息不直接发给其他会员,而是通过聊天室转发。聊天室在转发时执行协调逻辑:文本消息过滤不雅字符,图片消息检查大小是否超限。这种设计使得过滤规则的变化只影响 ChatRoom 类,会员类完全不受影响。未来要新增会员类型(比如管理员,可以发送公告),只需新建子类并在 ChatRoom 中添加对应的处理逻辑即可。
优缺点
优点:
| 优点 | 说明 |
|---|---|
| 简化对象交互 | 网状关系变成星形,复杂度大幅降低 |
| 各同事解耦 | 同事类之间不直接引用,可以独立变化和复用 |
| 减少子类生成 | 交互逻辑集中在中介者中,无需为每种交互关系创建子类 |
| 简化同事类设计 | 每个同事只需关心自身业务,交互细节由中介者处理 |
缺点:
- 具体中介者类可能变得非常复杂——所有同事之间的交互细节都集中在这一个类里,维护成本高。中介者本质上是用一个类的复杂性换取一组类之间的复杂性
中介者模式将分散的复杂性集中到了一处。如果交互逻辑本身就很复杂,中介者类会变成一个臃肿的「上帝类」。使用时需要权衡:网状耦合和中介者膨胀,哪个更难维护。
适用场景
- 系统中对象之间存在复杂的引用关系,依赖结构混乱且难以理解
- 一个对象引用了太多其他对象且直接通信,导致难以复用
- 想通过一个中间类来封装多个类中的行为,而不想为每种交互创建子类
在实际开发中,中介者模式常见于事件驱动的 GUI 应用——多个界面组件之间存在复杂的交互关系(如勾选某个复选框时禁用某些输入框、联动更新某个标签),引入一个中介者类来统一管理这些交互可以显著降低组件之间的耦合。MVC 架构中的 Controller 也是中介者的一个典型应用——在 Java EE 的 Struts 框架中,Action 类就充当了 JSP 页面与业务对象之间的中介者。
中介者与迪米特法则
回顾我们在第一节学过的迪米特法则(LoD):一个对象应该对其他对象保持最少的了解。中介者模式正是迪米特法则的典型应用——引入中介者后,每个同事对象的「朋友圈」从原来的所有交互对象缩减为唯一一个中介者。
前面提到 MVC 中的 Controller 既是观察者模式的一部分,也是中介者。这并不矛盾——两种模式从不同角度描述了 MVC 的运作:从 Model 到 View 的数据通知机制是观察者模式,Controller 对 Model 与 View 之间交互的协调是中介者模式。一个架构可以同时运用多种设计模式,它们各有侧重、互为补充。
模板方法模式
固定流程,可变细节
前两种模式——观察者和中介者——都是关于运行时对象之间的协作,通过组合和委托来实现。现在我们转向一种截然不同的行为型模式:它不依赖对象组合,而是利用继承来定义行为的结构。
去银行办业务,不管是取款、存款还是转账,流程总是三步:取号排队 → 办理业务 → 评分反馈。第一步和第三步对所有业务类型都一样,只有中间的「办理业务」因具体需求而不同。
如果每种业务都从头写一遍完整流程,取号和评分的代码就会重复出现在每个实现中。更好的做法是:把固定的流程写在父类里,把可变的步骤声明为抽象方法,留给子类去实现。这就是模板方法模式——它可能是所有设计模式中最简单的一个,却也是使用频率最高的之一。
模式定义
模板方法模式
模板方法模式(Template Method Pattern)定义一个操作中算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.
属于类行为型模式——注意,它是通过类的继承而非对象的组合来实现的,结构中只有继承关系,没有对象关联关系。
模式结构
模板方法模式的结构极其简洁,只有两个角色:
classDiagram
class AbstractClass {
+templateMethod() void
+primitiveOp1()* void
#primitiveOp2() void
#hookMethod() boolean
}
class ConcreteClassA {
+primitiveOp1() void
#primitiveOp2() void
}
class ConcreteClassB {
+primitiveOp1() void
#hookMethod() boolean
}
AbstractClass <|-- ConcreteClassA
AbstractClass <|-- ConcreteClassB
- AbstractClass(抽象类):定义模板方法和各种基本方法
- ConcreteClass(具体子类):实现父类中的抽象方法,可选地覆盖钩子方法
模板方法模式要求负责总体设计的开发者和负责具体实现的开发者之间进行协作:前者在抽象类中定义算法的轮廓和骨架,后者在具体子类中填充各个步骤的实现。
模板方法与基本方法
模板方法(Template Method)是定义在抽象类中的、将基本方法组合成一个完整算法的方法。它规定了算法的执行步骤和顺序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public abstract class AbstractClass { // 模板方法——定义算法骨架 public final void templateMethod() { primitiveOperation1(); // 具体方法:固定步骤 primitiveOperation2(); // 抽象方法:子类必须实现 if (hookMethod()) { // 钩子方法:子类可选覆盖 primitiveOperation3(); } } // 具体方法——已有默认实现 protected void primitiveOperation1() { // 固定的实现代码 } // 抽象方法——子类必须覆盖 protected abstract void primitiveOperation2(); // 钩子方法——默认空实现,子类可选覆盖 protected void primitiveOperation3() { } // 钩子方法——返回 boolean,控制流程分支 protected boolean hookMethod() { return true; } } |
具体子类只需实现抽象方法,并根据需要覆盖钩子方法:
1 2 3 4 5 6 7 8 9 10 11 | public class ConcreteClass extends AbstractClass { @Override protected void primitiveOperation2() { // 子类特有的实现 } @Override protected void primitiveOperation3() { // 可选:覆盖钩子方法以添加额外行为 } } |
基本方法(Primitive Method)是组成算法的各个步骤,分为三种:
| 类型 | 特征 | 定义位置 |
|---|---|---|
| 抽象方法(Abstract Method) | 声明但不实现,子类必须覆盖 | 抽象类声明,子类实现 |
| 具体方法(Concrete Method) | 已有默认实现,子类通常不需覆盖 | 抽象类实现 |
| 钩子方法(Hook Method) | 有默认实现(通常为空或返回默认值),子类可选覆盖 | 抽象类提供默认,子类选择性覆盖 |
三种方法的分工很清楚:抽象方法是「必须填的空」,具体方法是「已经填好的」,钩子方法是「可填可不填的」。
钩子方法:子类的「否决权」
钩子方法是模板方法模式中最精妙的设计。它允许子类在不改变算法结构的前提下,影响父类模板方法的执行流程。
最常见的钩子方法是返回 boolean 值的「开关方法」。例如一个文档处理流程:打开 → 显示 → 打印,但「是否打印」可以由子类决定:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public abstract class DocumentProcessor { // 模板方法 public final void process() { open(); display(); if (isPrint()) { // 钩子方法控制是否打印 print(); } } protected abstract void open(); protected abstract void display(); protected abstract void print(); // 钩子方法——默认需要打印,子类可覆盖为 false 跳过 protected boolean isPrint() { return true; } } |
子类只需覆盖 isPrint() 返回 false,就能跳过打印步骤——不需要修改模板方法本身。这种机制让子类在「服从算法骨架」的同时拥有了局部的「否决权」。
另一种常见的钩子方法是空方法——父类提供一个什么都不做的默认实现,子类可以选择性地覆盖它来「挂钩」额外行为。由于面向对象的多态性,运行时子类的方法会覆盖父类的方法,从而实现子类对父类行为的反向控制。
实例:银行业务办理
回到开头的银行例子。所有业务共享「取号 → 办理 → 评分」的流程,只是「办理」步骤因业务而异:
classDiagram
class BankBusiness {
<<abstract>>
+process() void
-takeNumber() void
+transact()* void
-evaluate() void
}
class Deposit {
+transact() void
}
class Withdrawal {
+transact() void
}
class Transfer {
+transact() void
}
BankBusiness <|-- Deposit
BankBusiness <|-- Withdrawal
BankBusiness <|-- Transfer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | public abstract class BankBusiness { // 模板方法 public final void process() { takeNumber(); // 具体方法:取号 transact(); // 抽象方法:办理具体业务 evaluate(); // 具体方法:评分 } private void takeNumber() { System.out.println("取号排队"); } protected abstract void transact(); // 子类实现 private void evaluate() { System.out.println("请对本次服务评分"); } } public class Deposit extends BankBusiness { @Override protected void transact() { System.out.println("办理存款业务"); } } |
新增业务类型(如理财、贷款)只需新增一个子类并实现 transact(),不影响已有代码——完美的开闭原则。
同样的思路也适用于数据库操作。数据库操作通常遵循「连接 → 打开 → 使用 → 关闭」的固定流程。不同数据库(如 SQL Server 和 Oracle)只在连接方式(connDB())上有区别,其余步骤基本一致。将 connDB() 声明为抽象方法,openDB() 和 closeDB() 作为具体方法,就构成了一个完整的数据库操作模板。
好莱坞原则
模板方法模式体现了一个重要的设计原则——好莱坞原则(Hollywood Principle):
「别打电话给我们,我们会打给你。」(Don't call us, we'll call you.)
在好莱坞,演员去试镜后不会主动打电话催问结果,而是等导演来通知。在模板方法模式中,子类不会主动调用父类的方法,而是由父类的模板方法在合适的时机调用子类的方法。子类只需实现被要求的抽象方法,执行时机完全由父类控制。
这种「父类控制流程,子类提供实现」的机制也称为控制反转(Inversion of Control, IoC)——前面钩子方法一节中提到的子类对父类的「反向控制」就是它的一种体现。如果你接触过 Spring 框架,它的核心 IoC 容器正是这一原则的架构级应用——开发者不需要主动调用框架的方法,而是在框架定义的「模板」中填充自己的逻辑,由框架在合适的时机来调用。
好莱坞原则与依赖倒转原则(DIP)有相似之处——都强调「高层不应该依赖低层的细节」。区别在于 DIP 关注的是依赖方向(面向抽象编程),好莱坞原则关注的是控制流方向(父类/框架控制调用时机)。
优缺点与适用场景
优点:
| 优点 | 说明 |
|---|---|
| 代码复用 | 公共部分在父类中实现一次,所有子类共享 |
| 反向控制 | 通过子类覆盖父类方法,由父类控制调用顺序,符合开闭原则 |
| 符合单一职责 | 每个子类只负责自己的变化部分 |
| 恰当使用继承 | 将可复用的一般性行为集中到父类,特殊化行为下放到子类 |
缺点:
- 每种不同的实现都需要一个子类,类的数量可能膨胀
- 设计更加抽象——算法流程分散在父类和子类中,理解时需要同时阅读两处代码
适用场景:
- 一次性实现算法的不变部分,将可变行为留给子类
- 各子类中的公共行为应提取到父类以避免代码重复
- 需要对一些复杂算法进行分割,将固定部分设计为模板方法,可变细节由子类实现
- 需要控制子类的扩展——子类只能在特定步骤中定制行为
模板方法模式在框架设计中极为常见。JUnit 的 TestCase 类就是一个经典案例——runBare() 方法定义了 setUp() → runTest() → tearDown() 的固定流程,开发者只需覆盖这三个方法来编写测试用例:
1 2 3 4 5 6 7 8 9 | // JUnit TestCase 的核心——模板方法 public void runBare() throws Throwable { setUp(); // 钩子方法:测试前的准备工作 try { runTest(); // 抽象方法:执行测试逻辑 } finally { tearDown(); // 钩子方法:测试后的清理工作 } } |
Spring Framework 也大量使用模板方法模式,如 JdbcTemplate 封装了获取连接、执行 SQL、处理异常、释放资源的流程,用户只需提供 SQL 和结果映射逻辑。Java Servlet 中 HttpServlet 的 service() 方法同样是模板方法——根据请求类型分发到 doGet()、doPost() 等方法,开发者覆盖对应方法即可。
模板方法 vs 策略模式
两者都实现了算法的可替换性,但机制不同:
- 模板方法用继承——子类覆盖父类的抽象方法来改变部分步骤
- 策略模式用组合——将整个算法封装在策略对象中,通过替换策略对象来改变行为
模板方法适合「骨架固定、部分步骤可变」的场景,策略模式适合「整个算法可替换」的场景。两者的选择,本质上是「继承 vs 组合」这个经典命题的又一次体现。
三种模式纵览
本节的三种模式都属于行为型模式,但解决的问题各不相同:
| 维度 | 观察者模式 | 中介者模式 | 模板方法模式 |
|---|---|---|---|
| 核心问题 | 一对多通知 | 多对多协调 | 算法骨架复用 |
| 关键机制 | Subject 维护 Observer 列表并广播 | Mediator 封装 Colleague 间的交互 | 父类定义模板方法,子类覆盖基本方法 |
| 模式类型 | 对象行为型 | 对象行为型 | 类行为型 |
| 实现方式 | 组合 + 接口 | 组合 + 接口 | 继承 |
| 耦合方向 | Observer → Subject | Colleague → Mediator | 子类 → 父类 |
| 典型场景 | 事件处理、MVC、数据绑定 | 聊天室、GUI 组件协调 | 框架设计、固定流程 |
设计工具箱更新:
| 层次 | 内容 |
|---|---|
| OO 基础 | 抽象、封装、多态、继承 |
| OO 原则 | 封装变化、面向接口编程、组合优于继承、好莱坞原则 |
| OO 模式 | 策略、简单工厂、工厂方法、抽象工厂、建造者、原型、状态、命令、观察者、中介者、模板方法 |