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

发布时间:2026/10/13 12:24:15
行为型设计模式详解:策略、模板方法、观察者、迭代器、责任链 设计模式系列写到第四篇终于轮到行为型模式了。前几篇我们把创建型怎么把对象造出来和结构型怎么把对象组合起来讲完了这一篇开始讲行为型模式的“上篇”。如果你是跟着这个系列一路看过来的应该已经感受到一个规律创建型模式关注的是“对象的诞生”结构型模式关注的是“对象的静态关系”。而行为型模式完全不同它关注的是对象之间在运行时怎么协作、怎么分配责任、怎么通信。换句话说前两类模式回答的是“对象长什么样”行为型模式回答的是“对象在一起怎么干活”。行为型模式一共有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 ListWeatherObserver 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。我的经验是两条铁律谁注册谁负责注销。注册和注销必须成对出现最好写在同一个方法里比如start()和destroy()。如果无法保证成对出现使用弱引用。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 IterableOrderInfo { private ListOrderInfo orders new ArrayList(); public void add(OrderInfo order) { orders.add(order); } Override public IteratorOrderInfo iterator() { return new PaidOrderIterator(); } // 迭代器按时间倒序并过滤未支付 private class PaidOrderIterator implements IteratorOrderInfo { 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”的顺序传递。结果有一次配置错误某个级别的过滤器没有注册到链上大量日志直接穿过链尾既没输出也没处理排查了半天才找到问题。从那以后我给自己定了几条规矩责任链必须有明确的链尾兜底逻辑。要么在handle的最后写清楚“无人处理时怎么办”要么在组装链时保证最后一个节点一定能处理。责任链不能太长。超过五六个节点调试和阅读成本会显著上升。如果业务本身没有天然的“传递”语义不要硬套责任链。节点顺序很重要。责任链上的顺序有时候就是业务规则的一部分组装链的地方应该集中配置并且要写注释说明为什么是这个顺序。7. 五种模式放在一起怎么选写到这里我们把五个模式都过了一遍。最后用一个表格横向对比一下再聊聊团队协作时怎么选型。模式核心思想适用场景主要风险策略模式算法族封装可整体替换支付渠道、优惠规则、排序算法策略类爆炸工厂膨胀模板方法模式骨架固定步骤留给子类订单处理流程、数据迁移流程继承层级深骨架改动影响大观察者模式一对多状态通知事件广播、消息推送、UI刷新监听器泄漏调用链难追踪迭代器模式遍历逻辑与结构解耦自定义集合遍历、过滤排序遍历并发修改异常迭代器复用出错责任链模式请求沿链传递直到有人处理审批流、日志过滤器、拦截器链尾无兜底顺序错误难发现选型时我的判断顺序一般是这样的先看变化发生的位置。如果是“整个算法都能换”选策略模式如果是“算法骨架稳定只有某几步会变”选模板方法模式如果变化的是“对象状态变化后谁要跟着动”选观察者模式。再看调用关系。如果多个对象之间的交互是一对多的广播观察者模式最顺手如果请求和处理器之间是传递关系责任链模式更贴切如果只是想让外部顺序访问内部元素迭代器模式就够了。最后看团队熟悉程度。设计模式的引入不是炫技要考虑队友后续的维护成本。一个团队如果用策略模式和模板方法用得顺手那就优先用这两种解决大部分问题观察者模式要慎用因为它会切断调用方和被调用方之间的直接联系新人接手代码时经常找不到逻辑在哪。责任链模式适合业务本身有“层级审批”语义的场景没事别硬造链。我自己在项目里的习惯是策略、模板方法、迭代器这三种模式几乎每天都会用观察者和责任链则要结合具体业务场景判断绝不为了用模式而用模式。这篇先讲到这里。行为型模式还有六个——状态、命令、备忘录、访问者、中介者、解释器其中状态模式和命令模式在真实项目里出现频率非常高等下一篇接着聊。最后分享一个小技巧学设计模式别只看结构试着在脑子里把每个模式对应到一个你写过的真实代码场景。能对上号说明你真正理解了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询