ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

行为型设计模式详解:策略、模板方法、观察者、迭代器、责任链

行为型设计模式详解:策略、模板方法、观察者、迭代器、责任链

设计模式系列写到第四篇,终于轮到行为型模式了。前几篇我们把创建型(怎么把对象造出来)和结构型(怎么把对象组合起来)讲完了,这一篇开始讲行为型模式的“上篇”。

如果你是跟着这个系列一路看过来的,应该已经感受到一个规律:创建型模式关注的是“对象的诞生”,结构型模式关注的是“对象的静态关系”。而行为型模式完全不同,它关注的是对象之间在运行时怎么协作、怎么分配责任、怎么通信。换句话说,前两类模式回答的是“对象长什么样”,行为型模式回答的是“对象在一起怎么干活”。

行为型模式一共有11个,数量是所有类别里最多的。一口气全讲完既不现实,也容易消化不良。这篇我先把其中5个最常见、面试和日常开发里出现频率最高的讲清楚:策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式。剩下的状态模式、命令模式、备忘录模式、访问者模式、中介者模式、解释器模式,放到下篇继续。

每个模式我会按照“解决了什么问题 → 代码怎么落地 → 实战中要注意什么”的顺序来讲,尽量把我在实际项目里踩过的坑、用过的变体写法都交代清楚。不管是刚开始学设计模式的新人,还是想快速回顾的老手,这篇都适合你。

1. 先搞清楚:行为型模式到底在解决什么问题

1.1 从“对象怎么搭”到“对象怎么协作”

前面说的创建型和结构型模式,本质上都是在解决静态结构问题——类和类之间怎么继承、怎么组合、怎么代理。这些关系在你把代码写完的那一刻就已经确定了。

行为型模式不一样。它关注的是动态行为——程序运行过程中,对象之间怎么传递消息、怎么分配职责、怎么互相调用。同一个对象,在不同策略下表现不同;同一个请求,在不同处理器链上走向不同。这些都是在运行时才决定的。

我打个比方。创建型模式像生产线设计——决定零件怎么生产;结构型模式像装配图——决定零件怎么拼装;行为型模式则是流水线上的调度规则——决定了每个工位什么时候动作、做完之后传给谁、遇到异常怎么办。

1.2 行为型模式的“两条主线”

11个行为型模式其实可以分成两类,理解了这个分类,学习成本能降低一半:

第一类:算法与行为的封装

这类模式的核心思想是把“变化的算法/行为”从“稳定的主体”中抽离出来。代表有:

  • 策略模式:把一个算法的不同实现封装成独立的策略对象
  • 模板方法模式:把算法的骨架定死,把可变的步骤留给子类
  • 状态模式:把对象在不同状态下的行为封装成独立的状态对象
  • 命令模式:把一次请求/操作封装成一个独立对象

第二类:对象间的通信与职责分配

这类模式的核心思想是解决“对象之间怎么说话、谁该干活”的问题。代表有:

  • 观察者模式:一对多的依赖通知
  • 责任链模式:请求沿着链传递,找到能处理的人
  • 迭代器模式:提供统一遍历方式,屏蔽内部结构
  • 中介者模式:通过中介对象转发多方通信
  • 访问者模式:在不修改对象结构的前提下增加新操作

这两条主线下面写代码时,思路是完全不同的。封装类的主线,你得多考虑“哪些是稳定的、哪些是变化的”;通信类的主线,你得多考虑“谁通知谁、谁处理谁、顺序是什么”。

理解了这两条线,接下来每个模式看起来都不会觉得是孤立的,能串成一个体系。

2. 策略模式:把“怎么做”从“做什么”手里拿过来

2.1 一个支付场景的演化过程

策略模式是我在实际项目中用得最多的行为型模式。我们还是拿一个真实的业务场景来说——订单支付。

假设某电商项目一开始只有一个余额支付渠道,代码很简单:

public class PaymentService { public boolean pay(OrderInfo order) { // 从账户余额扣款 return balanceService.deduct(order.getUserId(), order.getAmount()); } }

第二天产品经理来了:要接入银行网关支付。于是代码变成:

public class PaymentService { public boolean pay(OrderInfo order, PayChannel channel) { if (channel == PayChannel.BALANCE) { return balanceService.deduct(...); } else if (channel == PayChannel.BANK) { return bankGateway.pay(...); } return false; } }

第三天,又来了扫码支付、第三方支付……这个else if链越来越长。你心里已经开始发毛了:新增一个渠道就要改一次PaymentService,测试要回归所有渠道,而且万一某个渠道的服务注入方式还不一样,这个类会膨胀到没法维护。

这不是“代码洁癖”,这是典型的扩展点被埋没问题。支付逻辑的主体流程是稳定的——验证订单、调用渠道、记录结果。变化的是“具体调哪个渠道、渠道内部怎么走”。

把变化封装起来,这就是策略模式的核心思路。

2.2 策略模式的标准结构

策略模式有三个角色:

  • 策略接口:定义算法族共有的行为
  • 具体策略:实现接口,每种算法一个类
  • 上下文:持有策略引用,调用方只需面向接口操作

对应到支付场景,代码长这样:

// 策略接口 public interface PayStrategy { boolean pay(OrderInfo order); } // 具体策略:余额支付 public class BalancePayStrategy implements PayStrategy { @Override public boolean pay(OrderInfo order) { // 扣减余额、记流水 return true; } } // 具体策略:扫码支付 public class QrPayStrategy implements PayStrategy { @Override public boolean pay(OrderInfo order) { // 调用扫码支付网关 return true; } } // 具体策略:银行卡支付 public class BankPayStrategy implements PayStrategy { @Override public boolean pay(OrderInfo order) { // 调用银行接口 return true; } }

上下文类不再关心具体策略,只面向PayStrategy编程:

public class PaymentService { private PayStrategy strategy; public void setStrategy(PayStrategy strategy) { this.strategy = strategy; } public boolean execute(OrderInfo order) { return strategy.pay(order); } }

客户端选择策略的职责,通常交给工厂或者配置中心,而不是让PaymentService自己判断。

PayStrategy strategy = PayStrategyFactory.getStrategy(order.getChannel()); paymentService.setStrategy(strategy); paymentService.execute(order);

这套结构写完之后,新增支付渠道只需要做两件事:写一个实现PayStrategy的新类,在工厂里注册一下。开闭原则立刻就体现了——对扩展开放,对修改关闭。

2.3 我把过的两个坑

第一个坑:把所有判断都塞进工厂

策略模式最容易被反向滥用。有人把所有渠道的构建逻辑都堆到工厂里,导致工厂本身变成了一个巨大的if/else。解决思路是让工厂从配置中心读取“渠道类型 -> 策略Bean名称”的映射,用反射或者注册表来创建策略对象。

第二个坑:策略对象内部还是有大量公共代码

不同支付策略其实有很多共同的流程,比如订单状态校验、失败重试、流水记录。如果每个策略类都各自写一遍,就会出现大量重复代码。

我的做法是:写一个抽象基类,把公共流程放在基类,把差异逻辑留在子类。但注意,这已经是在往模板方法模式靠了。所以策略模式和模板方法模式经常一起出现——策略接口定边界,抽象基类定公共流程,具体子类实现差异。

3. 模板方法模式:算法骨架与可变步骤的分离

3.1 用“冲泡饮料”理解模板方法

如果说策略模式是把“整个算法”封装成对象,那么模板方法模式则是把算法的骨架定死在父类里,把可变步骤留给子类重写。

我经常用“冲泡饮料”来给学生讲这个模式:不管泡咖啡还是泡茶,流程都是一样的——烧水、冲泡、加料倒杯。但是具体冲泡什么、加什么料,每一步都不一样。把不变的骨架写在父类,把变化的步骤留给子类,这就是模板方法模式。

放到订单处理的场景里:

  • 稳定的骨架:校验订单 → 扣减库存 → 记录快照 → 通知客户
  • 可变的步骤:不同订单类型,校验规则不同、扣库存策略不同

3.2 模板方法 + 钩子方法的实际写法

public abstract class AbstractOrderProcessor { // 模板方法:定义算法骨架,建议用 final 防止子类篡改 public final void processOrder(OrderInfo order) { validate(order); deductStock(order); if (needNotify(order)) { // 钩子方法,子类可选覆盖 notifyCustomer(order); } createSnapshot(order); } // 抽象步骤:强制子类实现 protected abstract void validate(OrderInfo order); // 抽象步骤 protected abstract void deductStock(OrderInfo order); // 钩子方法:默认实现,子类不一定重写 protected boolean needNotify(OrderInfo order) { return true; } private void notifyCustomer(OrderInfo order) { // 发送短信/站内信 } private void createSnapshot(OrderInfo order) { // 保存订单快照到审计表 } }

子类只需要关注自己那部分差异:

public class FlashSaleOrderProcessor extends AbstractOrderProcessor { @Override protected void validate(OrderInfo order) { // 限购校验、活动时间校验 } @Override protected void deductStock(OrderInfo order) { // 锁定活动库存 } @Override protected boolean needNotify(OrderInfo order) { // 秒杀订单不发送消息,避免轰炸 return false; } }

这里有个关键点:processOrder用了final修饰。很多人忽略这个细节,导致子类把整个骨架都重写了,模板方法模式就失去了意义。骨架是约定,约定就不能被破坏。

3.3 模板方法 vs 策略模式到底怎么选

这是面试里很爱问的一个对比,也是实际开发里我经常要做的选择。

对比项模板方法模式策略模式
复用方式继承:复用父类骨架组合:通过持有策略对象复用
变化粒度算法内部某几步变化整个算法族可替换
扩展方式继承子类,覆写步骤新写策略类,替换上下文中的策略
与业务结合适合“骨架稳定,步骤多变”的场景适合“整个算法可替换”的场景
风险点继承层级深后不好维护策略类数量多后需要管理

个人看法:模板方法模式的耦合度比策略模式高,因为子类依赖父类,所以一旦骨架发生变化,所有子类都要跟着检查。策略模式通过组合,耦合度低一些,但代价是增加了一批策略类和上下文之间的交互代码。

实际项目中,我经常两个搭配着用:用策略模式定义大的算法边界,用模板方法模式处理策略内部的可变步骤。比如支付策略的接口用策略模式定义,但每个支付策略内部都有固定流程,那就再抽一个模板基类。

4. 观察者模式:让状态变化自己“广播”出去

4.1 气象站的经典结构

观察者模式应该是最容易理解的行为型模式了。它的核心思想就一句话:当一个对象的状态发生变化时,所有依赖它的对象都会自动收到通知。一对多的依赖关系,这边状态一变,那边所有观察者都被触发。

经典的教学案例是气象站:WeatherData负责采集气象数据,数据一变,DisplayBoard(显示面板)、AlertSystem(预警系统)等多个观察者都要更新。

用代码写出来是这样一个结构:

// 观察者接口 public interface WeatherObserver { void update(float temperature, float humidity, float pressure); } // 被观察者(主题) public class WeatherData { private List<WeatherObserver> observers = new ArrayList<>(); private float temperature; private float humidity; private float pressure; public void register(WeatherObserver o) { observers.add(o); } public void unregister(WeatherObserver o) { observers.remove(o); } public void setMeasurements(float temperature, float humidity, float pressure) { this.temperature = temperature; this.humidity = humidity; this.pressure = pressure; notifyObservers(); } private void notifyObservers() { for (WeatherObserver observer : observers) { observer.update(temperature, humidity, pressure); } } }

观察者那边实现接口即可:

public class AlertSystem implements WeatherObserver { @Override public void update(float temperature, float humidity, float pressure) { if (temperature > 35f) { System.out.println("高温预警"); } } }

4.2 我踩过好几次的监听器生命周期问题

观察者模式在真实项目中有一个非常隐蔽的坑:观察者注册了之后没有及时注销,造成内存泄漏。

举个例子,某后台管理系统里,用户登录后需要实时接收系统通知。很多人会在用户对象创建时把UserSessionListener注册到全局NotificationCenter上,但用户退出登录时忘了调用unregister。结果就是这个监听器一直被全局对象持有,用户明明已经退出了,GC还是无法回收它关联的一堆对象。在长期运行的服务器上,这就是个定时炸弹,内存悄悄涨,直到某天OOM。

我的经验是两条铁律:

  1. 谁注册,谁负责注销。注册和注销必须成对出现,最好写在同一个方法里,比如start()和destroy()。
  2. 如果无法保证成对出现,使用弱引用。Java里可以用WeakReference来包装观察者,或者直接用现成的WeakHashMap做监听器存储。

4.3 观察者模式在实战中的变体

实际开发中,很少真的手写一整套观察者接口。大多数时候我们会用事件总线、异步消息、发布订阅框架。Java自身的PropertyChangeSupport、各种消息中间件的Consumer/Producer模型,本质上都是观察者模式的扩展。

但不管用什么工具,核心的语义没变:发布者不关心谁在听,订阅者不关心谁在发声,双方只靠事件类型和内容进行松耦合对接。

这带来一个隐蔽的调试难度:事件一多,代码的调用链就变得很“飘”。你看到一个现象发生了,但不知道是哪个发布者触发、经过了几层异步转发。我的建议是:观察者模式适合跨模块的、低频次的、一对多的通知。如果模块内部逻辑强相关,高频调用,硬套观察者模式会让代码变得特别难追踪,这时候不如直接方法调用。

另外有一个实战细节:通知顺序。默认的ArrayList是按注册顺序遍历的,这本身没问题。但如果你在某个观察者的update里又触发了一个新事件,就可能出现迭代器修改冲突(ConcurrentModificationException),或者通知顺序错乱。稳妥做法是遍历之前对观察者列表做一份快照,或者让通知操作脱离当前线程去执行。

5. 迭代器模式:把“怎么遍历”从集合里拆出来

5.1 为什么需要迭代器模式

迭代器模式可能是行为型模式里最“没存在感”的一个,因为现代语言几乎都内置了迭代器语法,比如Java的Iterator接口、foreach语法糖、Python的__iter__和__next__。很多人以为这个模式已经过时了,其实不然。

迭代器模式的核心价值不是“怎么遍历一个数组”,而是把遍历逻辑和集合内部结构解耦。调用方不需要知道集合底层是数组、链表还是树,只需要调用hasNext()和next(),就能依次拿到元素。

当一个集合的数据结构足够简单时,确实用不上迭代器——直接用下标遍历就行。但真实项目里,集合往往不是铁板一块:

  • 有的集合存储的是多级树形结构,遍历时要按层级返回
  • 有的集合内部有多个分组,调用方需要按不同规则遍历
  • 有的集合在遍历过程中还要做过滤和转换

这时候把遍历算法内聚成一个迭代器对象,价值就体现出来了。

5.2 手写一个可迭代的订单历史

举个实际例子。某订单中心需要按时间倒序遍历用户的历史订单,同时只返回已支付订单。我定义了一个OrderHistory集合类,内部用List存储,但对外暴露一个定制迭代器:

public class OrderHistory implements Iterable<OrderInfo> { private List<OrderInfo> orders = new ArrayList<>(); public void add(OrderInfo order) { orders.add(order); } @Override public Iterator<OrderInfo> iterator() { return new PaidOrderIterator(); } // 迭代器,按时间倒序并过滤未支付 private class PaidOrderIterator implements Iterator<OrderInfo> { private int cursor = orders.size() - 1; private OrderInfo nextReady; @Override public boolean hasNext() { while (cursor >= 0) { OrderInfo candidate = orders.get(cursor); if (candidate.isPaid()) { nextReady = candidate; cursor--; return true; } cursor--; } return false; } @Override public OrderInfo next() { if (nextReady == null && !hasNext()) { throw new NoSuchElementException(); } OrderInfo result = nextReady; nextReady = null; return result; } } }

调用方不需要知道OrderHistory内部用了ArrayList、需要倒序访问、还要过滤状态。直接foreach循环:

for (OrderInfo order : orderHistory) { System.out.println(order.getOrderId()); }

这个把复杂遍历逻辑封装进迭代器的做法,让调用代码非常干净。

5.3 迭代器陷阱

迭代器最经典的坑是快速失败(fail-fast)与并发修改。Java里,如果在迭代过程中有其他线程修改了集合结构,ArrayList会抛出ConcurrentModificationException。这是保护机制,但很多人第一次遇到时都会懵。

我的建议是:单线程环境下,尽量在迭代过程中不要修改集合结构;确实需要改的,用CopyOnWriteArrayList或ConcurrentHashMap这类并发容器。还有一种做法是先把需要删除的元素收集起来,迭代结束后统一删除。

另外一个容易被忽略的点:迭代器是“一次性”的,不能回溯。如果你在一个场景里需要多次遍历同一个集合,每次都调用iterator()获取新迭代器,而不是复用同一个迭代器对象。

6. 责任链模式:请求自己找“能处理它的人”

6.1 从报销审批看责任链

责任链模式的思路非常贴近现实。拿报销审批来说,报销金额不同,需要不同级别的人审批:

  • 500元以下,项目经理批
  • 5000元以下,部门总监批
  • 50000元以下,总经理批
  • 超过50000,董事会批

最直觉的写法是if/else嵌套:

public void approve(ReimbursementRequest request) { if (request.getAmount() < 500) { manager.approve(request); } else if (request.getAmount() < 5000) { director.approve(request); } else if (...) { ... } }

问题很明显:审批规则一变就要改这个方法,而且随着规则变复杂,这个类会长得没法看。责任链模式的做法是把每个审批节点拆成独立的处理器,串成一条链,请求从链头进入,能处理的节点处理掉,处理不了就往后传。

6.2 责任链的代码结构

public abstract class ApprovalHandler { protected ApprovalHandler next; public void setNext(ApprovalHandler next) { this.next = next; } public void handle(ReimbursementRequest request) { if (canApprove(request)) { doApprove(request); } else if (next != null) { next.handle(request); } else { request.setApproved(false); System.out.println("无人能审批,直接驳回"); } } protected abstract boolean canApprove(ReimbursementRequest request); protected abstract void doApprove(ReimbursementRequest request); }

具体节点实现:

public class ManagerHandler extends ApprovalHandler { @Override protected boolean canApprove(ReimbursementRequest request) { return request.getAmount() <= 500; } @Override protected void doApprove(ReimbursementRequest request) { System.out.println("项目经理审批通过,金额: " + request.getAmount()); } } public class DirectorHandler extends ApprovalHandler { @Override protected boolean canApprove(ReimbursementRequest request) { return request.getAmount() > 500 && request.getAmount() <= 5000; } @Override protected void doApprove(ReimbursementRequest request) { System.out.println("总监审批通过,金额: " + request.getAmount()); } }

客户端把链组装起来:

ApprovalHandler manager = new ManagerHandler(); ApprovalHandler director = new DirectorHandler(); ApprovalHandler gm = new GeneralManagerHandler(); manager.setNext(director); director.setNext(gm); manager.handle(request);

这套写法的好处非常直接:新增审批节点只需新写一个Handler类,然后在组装链的地方拼接一下,完全不用动已有节点的代码。

6.3 责任链的边界:什么时候该停

责任链模式用多了之后,有一个问题值得警惕:请求在链上传递时,如果没有任何节点能处理,就会走到链尾,表现就是“石沉大海”,或者被默认逻辑兜底。

我在一个日志框架项目里就遇到过这种情况。当时设计了一组日志过滤器,按“DEBUG → INFO → WARN → ERROR”的顺序传递。结果有一次配置错误,某个级别的过滤器没有注册到链上,大量日志直接穿过链尾,既没输出也没处理,排查了半天才找到问题。

从那以后,我给自己定了几条规矩:

  1. 责任链必须有明确的链尾兜底逻辑。要么在handle的最后写清楚“无人处理时怎么办”,要么在组装链时保证最后一个节点一定能处理。
  2. 责任链不能太长。超过五六个节点,调试和阅读成本会显著上升。如果业务本身没有天然的“传递”语义,不要硬套责任链。
  3. 节点顺序很重要。责任链上的顺序有时候就是业务规则的一部分,组装链的地方应该集中配置,并且要写注释说明为什么是这个顺序。

7. 五种模式放在一起怎么选

写到这里,我们把五个模式都过了一遍。最后用一个表格横向对比一下,再聊聊团队协作时怎么选型。

模式核心思想适用场景主要风险
策略模式算法族封装,可整体替换支付渠道、优惠规则、排序算法策略类爆炸,工厂膨胀
模板方法模式骨架固定,步骤留给子类订单处理流程、数据迁移流程继承层级深,骨架改动影响大
观察者模式一对多状态通知事件广播、消息推送、UI刷新监听器泄漏,调用链难追踪
迭代器模式遍历逻辑与结构解耦自定义集合遍历、过滤排序遍历并发修改异常,迭代器复用出错
责任链模式请求沿链传递直到有人处理审批流、日志过滤器、拦截器链尾无兜底,顺序错误难发现

选型时我的判断顺序一般是这样的:

先看变化发生的位置。如果是“整个算法都能换”,选策略模式;如果是“算法骨架稳定,只有某几步会变”,选模板方法模式;如果变化的是“对象状态变化后,谁要跟着动”,选观察者模式。

再看调用关系。如果多个对象之间的交互是一对多的广播,观察者模式最顺手;如果请求和处理器之间是传递关系,责任链模式更贴切;如果只是想让外部顺序访问内部元素,迭代器模式就够了。

最后看团队熟悉程度。设计模式的引入不是炫技,要考虑队友后续的维护成本。一个团队如果用策略模式和模板方法用得顺手,那就优先用这两种解决大部分问题;观察者模式要慎用,因为它会切断调用方和被调用方之间的直接联系,新人接手代码时经常找不到逻辑在哪。责任链模式适合业务本身有“层级审批”语义的场景,没事别硬造链。

我自己在项目里的习惯是:策略、模板方法、迭代器这三种模式,几乎每天都会用;观察者和责任链则要结合具体业务场景判断,绝不为了用模式而用模式。

这篇先讲到这里。行为型模式还有六个——状态、命令、备忘录、访问者、中介者、解释器,其中状态模式和命令模式在真实项目里出现频率非常高,等下一篇接着聊。最后分享一个小技巧:学设计模式别只看结构,试着在脑子里把每个模式对应到一个你写过的真实代码场景。能对上号,说明你真正理解了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进