行为型设计模式实战:备忘录、观察者、状态、策略、模板方法与访问者
1. 行为型模式的整体版图与下半场定位行为型模式在二十三种经典设计模式里占了十一个席位是整个模式体系中最庞大也最贴近业务逻辑的一类。上半场我们聊过了责任链、命令、解释器、迭代器和中介者这五个它们更多聚焦在“请求如何传递”“对象如何遍历”“多方如何解耦”这些偏结构调度的问题上。而下半场要讲的六个模式——备忘录、观察者、状态、策略、模板方法和访问者——则把重心放在了“对象行为如何随内外部条件变化”“算法如何灵活替换”“状态如何安全流转”这些更动态的层面。我刚开始接触这些模式的时候最大的困惑是分不清策略和状态也搞不懂观察者和中介者到底差在哪。后来在真实项目里反复踩坑才明白这些模式的名字虽然抽象但它们解决的其实都是日常开发中反复出现的痛点。比如一个订单系统要支持多种支付方式你写一堆 if-else 也能跑但每加一种支付方式就要改核心代码这就是策略模式要解决的问题。再比如一个 UI 组件需要在数据变化时自动刷新你手动调刷新方法也能用但一旦订阅方多了就容易漏调这就是观察者模式的用武之地。这篇文章我会按照“先讲清楚每个模式解决什么问题再给出可直接运行的代码实例最后补充我在实际项目里踩过的坑”这个思路来展开。每个模式我都会配一个贴近真实业务的场景而不是那种猫叫狗叫的玩具例子。代码以 Python 和 Java 为主因为这两种语言在业务开发中最常见读者可以直接复制到自己的项目里改改用。如果你正在准备面试、正在重构一坨祖传代码或者单纯想让自己写的模块更好扩展这篇内容应该能帮到你。注意设计模式不是银弹过度使用会让代码变得更复杂。我见过一个项目把简单的数据转换硬套了访问者模式结果新增一种数据类型要改五个文件。模式的价值在于应对变化如果某个维度根本不会变那就别为它设计扩展点。2. 备忘录模式让对象拥有“后悔药”能力2.1 备忘录模式到底解决什么问题备忘录模式的核心思想用一句话概括就是在不破坏封装的前提下捕获一个对象的内部状态并在该对象之外保存这个状态以便日后可以将该对象恢复到原先保存的状态。听起来有点绕我用一个实际场景来解释。假设你在做一个在线文档编辑器用户编辑到一半想撤销上一步操作。最朴素的做法是在每次编辑前把整个文档对象深拷贝一份存起来撤销时直接替换。但这样做有两个问题第一文档对象可能很大每次全量拷贝内存吃不消第二文档内部有些字段是私有的外部不应该直接访问。备忘录模式就是来解决这两个矛盾的——它让文档对象自己负责生成一个“快照”这个快照只包含需要恢复的字段而且快照的创建和恢复都由文档对象自己控制外部只负责保管快照不关心快照里有什么。这个模式有三个角色发起人Originator就是需要被保存状态的对象备忘录Memento是那个快照管理者Caretaker负责存储和传递备忘录但不能修改它。我刚开始学的时候觉得这不就是把对象序列化一下存起来吗后来才理解备忘录模式的关键在于“窄接口”和“宽接口”的区分——管理者只能看到备忘录的窄接口只能把它当做一个黑盒传递而发起人可以看到宽接口能读取里面的所有状态。这种访问控制才是备忘录模式的精髓。2.2 一个可撤销的文本编辑器实例下面这个例子用 Python 实现一个简易的文本编辑器支持输入文字和撤销操作。我特意把备忘录设计成只保存必要字段而不是整个对象。class EditorMemento: def __init__(self, content, cursor_pos): self._content content self._cursor_pos cursor_pos def get_content(self): return self._content def get_cursor_pos(self): return self._cursor_pos class Editor: def __init__(self): self._content self._cursor_pos 0 def type_text(self, text): self._content text self._cursor_pos len(text) def save(self): return EditorMemento(self._content, self._cursor_pos) def restore(self, memento): self._content memento.get_content() self._cursor_pos memento.get_cursor_pos() def show(self): print(f内容: {self._content}, 光标位置: {self._cursor_pos}) class History: def __init__(self): self._mementos [] def push(self, memento): self._mementos.append(memento) def pop(self): if not self._mementos: return None return self._mementos.pop() if __name__ __main__: editor Editor() history History() editor.type_text(你好) history.push(editor.save()) editor.type_text(世界) history.push(editor.save()) editor.type_text() editor.show() editor.restore(history.pop()) editor.show() editor.restore(history.pop()) editor.show()运行结果会依次显示输入后的状态、撤销一步后的状态、再撤销一步后的状态。这个实现里History 类就是管理者它只负责存取 EditorMemento完全不知道备忘录里有什么内容。Editor 类自己决定保存哪些字段、如何恢复封装性没有被破坏。2.3 备忘录模式的实操心得与避坑指南在实际项目里用备忘录模式有几个点特别容易踩坑。第一个是内存问题。如果你的对象状态很大而且保存频率很高备忘录会迅速吃光内存。我做过一个图形编辑器每次拖动图形都保存一次备忘录结果拖一个复杂图形时内存直接飙到几个 G。后来改成只在鼠标松开时保存并且对备忘录做了压缩存储才把内存降下来。所以备忘录的保存时机和存储策略一定要根据业务场景仔细设计。第二个坑是备忘录的深拷贝问题。如果你的对象内部有引用类型的字段保存备忘录时一定要做深拷贝否则恢复的时候会发现状态根本没变——因为备忘录里存的还是同一个引用。我见过一个同事写的备忘录模式恢复后数据完全没变化排查了半天才发现是浅拷贝导致的。Python 里可以用copy.deepcopyJava 里需要自己实现clone或者用序列化来做深拷贝。第三个经验是关于备忘录的版本管理。如果系统需要支持多级撤销和重做光靠一个栈是不够的需要两个栈分别管理撤销和重做。而且当用户执行新操作时重做栈要清空。这个逻辑听起来简单但实际写的时候很容易漏掉边界条件比如撤销到最底部时再点撤销应该没反应重做栈为空时点重做也应该没反应。这些边界情况一定要写单元测试覆盖到。提示备忘录模式常和命令模式配合使用。命令模式负责封装操作备忘录模式负责保存操作前的状态两者结合就能实现完整的撤销重做功能。如果你正在做编辑器、绘图工具或者任何需要撤销功能的系统这个组合非常值得掌握。3. 观察者模式数据变化时谁该被通知3.1 从一对多依赖说起观察者模式定义了一种一对多的依赖关系让多个观察者对象同时监听某一个主题对象。当主题对象的状态发生变化时所有观察者都会收到通知并自动更新。这个模式在 GUI 编程、事件驱动系统、消息推送等场景里无处不在可以说是行为型模式里应用最广泛的一个。我最早接触观察者模式是在做 Android 开发的时候那时候还不懂什么设计模式只是觉得setOnClickListener这种写法很神奇——我只需要注册一个监听器按钮被点击时我的代码就会被调用。后来学了观察者模式才明白这就是典型的一对多通知机制。再后来做后端开发消息队列的发布订阅、Spring 的事件机制、Redis 的键空间通知本质上都是观察者模式的变体。观察者模式的价值在于解耦。主题对象不需要知道具体有哪些观察者只需要维护一个观察者列表状态变化时遍历通知即可。观察者也不需要知道主题的具体实现只需要实现一个统一的更新接口。这样新增观察者时完全不用改主题的代码符合开闭原则。但这里有个关键点通知的顺序是不确定的如果你的业务逻辑依赖观察者的执行顺序那就不能依赖观察者模式或者需要在观察者内部再做一层排序。3.2 股票价格监控系统的完整实现下面用 Java 实现一个股票价格监控系统。当股票价格变化时所有订阅了该股票的投资者都会收到通知。这个例子贴近真实业务也方便你理解观察者模式的实际价值。import java.util.ArrayList; import java.util.List; interface Observer { void update(String stockName, double price); } interface Subject { void registerObserver(Observer o); void removeObserver(Observer o); void notifyObservers(); } class Stock implements Subject { private ListObserver observers new ArrayList(); private String name; private double price; public Stock(String name) { this.name name; } public void setPrice(double price) { this.price price; notifyObservers(); } Override public void registerObserver(Observer o) { observers.add(o); } Override public void removeObserver(Observer o) { observers.remove(o); } Override public void notifyObservers() { for (Observer o : observers) { o.update(name, price); } } } class Investor implements Observer { private String investorName; public Investor(String name) { this.investorName name; } Override public void update(String stockName, double price) { System.out.println(investorName 收到通知 stockName 当前价格 price); } } public class Main { public static void main(String[] args) { Stock stock new Stock(某科技股); Investor a new Investor(投资者A); Investor b new Investor(投资者B); stock.registerObserver(a); stock.registerObserver(b); stock.setPrice(100.5); stock.setPrice(102.3); stock.removeObserver(a); stock.setPrice(99.8); } }这个实现里Stock 是主题Investor 是观察者。当价格变化时所有注册的投资者都会收到通知。移除投资者 A 之后后续的价格变化就只有 B 能收到。这个结构非常清晰新增投资者类型时只需要实现 Observer 接口即可Stock 的代码完全不用动。3.3 观察者模式的常见问题与优化思路观察者模式用起来简单但有几个问题在实际项目中经常遇到。第一个是内存泄漏。如果观察者被销毁了但没有从主题的观察者列表里移除主题就会一直持有观察者的引用导致观察者无法被垃圾回收。在 Java 里这个问题特别常见因为开发者容易忘记调用removeObserver。解决办法是使用弱引用WeakReference来持有观察者或者在观察者的生命周期结束时主动取消订阅。我个人的习惯是在观察者的close或destroy方法里统一取消所有订阅避免遗漏。第二个问题是通知风暴。如果主题频繁变化而观察者又很多每次变化都全量通知会导致性能问题。我做过一个实时数据大屏数据每秒更新十次观察者有几十个结果每次更新都要遍历通知一遍CPU 直接跑满。后来改成批量通知加节流把一秒内的多次变化合并成一次通知性能立刻降下来了。所以如果你的场景是高频变化一定要考虑通知的频率控制。第三个问题是观察者的异常处理。如果某个观察者在update方法里抛了异常后面的观察者就收不到通知了。这个行为在大多数场景下是不合理的——一个观察者出错不应该影响其他观察者。解决办法是在通知循环里对每个观察者做 try-catch把异常隔离在单个观察者内部。当然如果业务上要求所有观察者要么都成功要么都失败那就需要引入事务机制这就超出观察者模式本身的范畴了。问题类型典型表现解决思路内存泄漏观察者已销毁但仍收到通知使用弱引用或主动取消订阅通知风暴高频变化导致 CPU 飙升批量通知、节流、合并变化异常中断一个观察者出错导致后续观察者收不到通知每个观察者独立 try-catch顺序依赖业务逻辑依赖观察者执行顺序不依赖观察者模式改用责任链或显式排序提示观察者模式在分布式系统里会演变成发布订阅模式中间多了一个消息代理Broker。如果你在做跨进程的事件通知不要直接用观察者模式应该引入消息队列。观察者模式适合进程内的对象间通知跨进程场景要考虑网络延迟、消息丢失、重复消费等问题。4. 状态模式把状态流转从 if-else 里解放出来4.1 状态模式的核心思想与适用场景状态模式允许一个对象在其内部状态改变时改变它的行为看起来就像对象修改了自己的类。这个模式的核心是把每个状态封装成一个独立的类每个状态类负责处理该状态下的行为以及定义从该状态可以转移到哪些其他状态。上下文对象持有一个当前状态的引用所有请求都委托给当前状态对象处理。我见过太多项目用一大坨 if-else 或者 switch-case 来管理状态流转代码长到需要滚动好几屏才能看完。每次新增一个状态就要在所有相关的方法里加分支改漏一处就出 bug。状态模式就是来治这个病的。它把每个状态的行为局部化到各自的类里新增状态时只需要加一个新类修改状态转移逻辑时也只需要改对应的状态类不会影响到其他状态。状态模式特别适合那些状态数量有限、状态之间转移规则明确、且每个状态下行为差异较大的场景。比如订单系统待支付、已支付、已发货、已完成、已取消、工作流引擎草稿、审批中、已通过、已驳回、游戏角色站立、行走、跳跃、攻击等等。但如果你的状态很少而且行为差异不大用状态模式反而会让代码更复杂这时候简单的枚举加 switch 就够了。4.2 订单状态流转的完整代码实现下面用 Python 实现一个订单状态流转系统。订单有四个状态待支付、已支付、已发货、已完成。每个状态下可以执行的操作不同比如待支付状态可以支付和取消已支付状态可以发货已发货状态可以确认收货。from abc import ABC, abstractmethod class OrderState(ABC): abstractmethod def pay(self, order): pass abstractmethod def ship(self, order): pass abstractmethod def confirm(self, order): pass abstractmethod def cancel(self, order): pass class PendingPaymentState(OrderState): def pay(self, order): print(支付成功订单进入已支付状态) order.set_state(PaidState()) def ship(self, order): print(订单未支付无法发货) def confirm(self, order): print(订单未支付无法确认收货) def cancel(self, order): print(订单已取消) order.set_state(CancelledState()) class PaidState(OrderState): def pay(self, order): print(订单已支付请勿重复支付) def ship(self, order): print(发货成功订单进入已发货状态) order.set_state(ShippedState()) def confirm(self, order): print(订单未发货无法确认收货) def cancel(self, order): print(已支付订单取消中退款处理) order.set_state(CancelledState()) class ShippedState(OrderState): def pay(self, order): print(订单已支付请勿重复支付) def ship(self, order): print(订单已发货请勿重复发货) def confirm(self, order): print(确认收货订单完成) order.set_state(CompletedState()) def cancel(self, order): print(已发货订单无法直接取消请联系客服) class CompletedState(OrderState): def pay(self, order): print(订单已完成) def ship(self, order): print(订单已完成) def confirm(self, order): print(订单已完成) def cancel(self, order): print(订单已完成无法取消) class CancelledState(OrderState): def pay(self, order): print(订单已取消) def ship(self, order): print(订单已取消) def confirm(self, order): print(订单已取消) def cancel(self, order): print(订单已取消) class Order: def __init__(self): self._state PendingPaymentState() def set_state(self, state): self._state state def pay(self): self._state.pay(self) def ship(self): self._state.ship(self) def confirm(self): self._state.confirm(self) def cancel(self): self._state.cancel(self) if __name__ __main__: order Order() order.pay() order.ship() order.confirm() order.cancel()这个实现里每个状态类只关心自己状态下能做什么、不能做什么以及做完之后转移到哪个状态。Order 类只负责把请求委托给当前状态完全不包含任何状态判断逻辑。新增一个“退款中”状态时只需要加一个 RefundingState 类并在相关状态类里加上转移到它的逻辑Order 类一行都不用改。4.3 状态模式的实操经验与常见误区状态模式用起来很优雅但有几个误区需要特别注意。第一个误区是把状态模式当成万能药。如果你的状态转移逻辑非常简单比如只有两三个状态且转移规则固定那用状态模式就是杀鸡用牛刀。我见过一个项目只有“开”和“关”两个状态硬是写了两个状态类加一个上下文类代码量比直接写 if-else 多了三倍。所以用之前先评估一下状态数量是否超过三个状态之间的转移是否复杂每个状态下的行为差异是否明显如果答案都是否那就别用。第二个误区是状态类之间的循环依赖。如果状态 A 的转移逻辑里需要创建状态 B状态 B 的转移逻辑里又需要创建状态 A就会形成循环导入。在 Python 里这会直接导致 ImportError在 Java 里虽然不会报错但代码结构会很乱。解决办法是把状态转移的逻辑集中到一个状态机工厂里或者用字符串标识状态而不是直接引用类。我个人的习惯是用一个字典来映射状态标识和状态类状态类内部只返回下一个状态的标识由上下文负责实例化。第三个经验是关于状态持久化。如果你的订单状态需要存数据库那状态类本身是不能直接序列化的你需要在数据库里存一个状态标识字符串读取时再根据标识恢复成对应的状态对象。这个转换过程最好封装成一个工厂方法避免在业务代码里到处写 if-else 来判断状态标识。另外状态转移的历史记录也很重要很多业务场景需要追溯订单经历了哪些状态、每次转移的时间是什么。这个可以在上下文类的set_state方法里统一记录不需要每个状态类自己处理。注意状态模式和策略模式在类结构上几乎一模一样都是上下文持有一个抽象接口的引用把请求委托给它。区别在于意图策略模式是让客户端选择算法算法之间通常没有依赖关系状态模式是对象自己根据内部状态选择行为状态之间通常有明确的转移关系。面试时如果被问到这两个模式的区别从意图和转移关系入手回答就能说到点子上。5. 策略模式算法替换的艺术5.1 策略模式解决的核心痛点策略模式定义了一系列算法把它们各自封装起来并且使它们可以互相替换。策略模式让算法的变化独立于使用算法的客户端。这个模式的核心价值在于把“做什么”和“怎么做”分开——客户端知道要完成什么任务但不需要知道具体用哪种算法来完成。我在实际项目里最常用策略模式的场景是支付方式选择。一个电商系统需要支持微信支付、支付宝、银行卡等多种支付方式每种支付方式的调用逻辑完全不同。如果用 if-else 来写每加一种支付方式就要改支付核心类而且所有支付逻辑挤在一个方法里维护起来非常痛苦。用策略模式之后每种支付方式是一个独立的策略类支付核心类只负责根据用户选择调用对应的策略新增支付方式时只需要加一个策略类并注册到工厂里。策略模式和状态模式经常被拿来比较因为它们的类结构确实很像。但策略模式的策略之间通常是平等的、可互换的客户端主动选择用哪个策略而状态模式的状态之间通常有转移关系对象自己根据当前状态决定行为。简单说策略模式是“我选一个工具来用”状态模式是“我到了某个阶段就自动做对应的事”。5.2 多支付方式切换的代码实例下面用 Java 实现一个支付策略系统支持微信、支付宝和银行卡三种支付方式。代码里还加了一个策略工厂方便根据用户选择获取对应的策略。interface PaymentStrategy { void pay(double amount); } class WeChatPay implements PaymentStrategy { Override public void pay(double amount) { System.out.println(使用微信支付 amount 元); } } class AliPay implements PaymentStrategy { Override public void pay(double amount) { System.out.println(使用支付宝支付 amount 元); } } class BankCardPay implements PaymentStrategy { Override public void pay(double amount) { System.out.println(使用银行卡支付 amount 元); } } class PaymentFactory { public static PaymentStrategy getStrategy(String type) { switch (type) { case wechat: return new WeChatPay(); case alipay: return new AliPay(); case bank: return new BankCardPay(); default: throw new IllegalArgumentException(不支持的支付方式: type); } } } class PaymentContext { private PaymentStrategy strategy; public void setStrategy(PaymentStrategy strategy) { this.strategy strategy; } public void executePayment(double amount) { if (strategy null) { throw new IllegalStateException(请先选择支付方式); } strategy.pay(amount); } } public class Main { public static void main(String[] args) { PaymentContext context new PaymentContext(); context.setStrategy(PaymentFactory.getStrategy(wechat)); context.executePayment(199.0); context.setStrategy(PaymentFactory.getStrategy(alipay)); context.executePayment(299.0); context.setStrategy(PaymentFactory.getStrategy(bank)); context.executePayment(399.0); } }这个实现里PaymentContext 是上下文它持有一个 PaymentStrategy 的引用。客户端通过工厂获取策略并设置到上下文中然后调用executePayment执行支付。新增一种支付方式时只需要实现 PaymentStrategy 接口并在工厂里注册即可PaymentContext 完全不用改。5.3 策略模式的进阶用法与避坑要点策略模式在实际项目中有几个进阶用法值得掌握。第一个是用枚举来实现策略。如果你的策略数量有限且不会频繁增加可以用枚举来实现策略接口这样既能保证策略的唯一性又能方便地做遍历和查找。Java 里枚举可以实现接口每个枚举常量提供不同的实现这种写法比单独的类更简洁。第二个是用 Lambda 表达式简化策略。如果策略接口只有一个方法函数式接口那就可以用 Lambda 表达式来创建策略不需要为每个策略单独写一个类。这在策略逻辑比较简单的时候特别方便。比如排序策略你可以直接传(a, b) - a - b表示升序传(a, b) - b - a表示降序代码量大幅减少。第三个坑是策略的初始化成本。如果策略对象的创建成本很高比如需要加载配置文件、建立数据库连接那就不要在每次使用时都新建策略而应该把策略缓存起来复用。我做过一个风控系统每个风控策略都需要加载规则模型创建一次要好几秒如果每次请求都新建策略系统根本扛不住。后来改成策略单例加懒加载性能问题就解决了。策略实现方式适用场景优点缺点独立类策略逻辑复杂、数量多结构清晰、易维护类文件多枚举策略数量固定、逻辑简单简洁、可遍历扩展需要改枚举Lambda函数式接口、逻辑简单代码量少可读性稍差工厂缓存策略创建成本高性能好需要管理缓存生命周期提示策略模式常和简单工厂模式配合使用工厂负责创建策略上下文负责执行策略。但要注意工厂里的 switch-case 并没有消除只是把变化点集中到了一个地方。如果策略数量非常多可以考虑用配置文件或注解来注册策略实现完全动态的加载。6. 模板方法模式把骨架定下来把细节留出去6.1 模板方法模式的设计哲学模板方法模式在一个方法中定义一个算法的骨架而将一些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的情况下重新定义算法中的某些步骤。这个模式的核心是“父类定流程子类填细节”。我在做数据处理系统的时候大量使用了模板方法模式。一个典型的数据处理流程是读取数据、校验数据、转换数据、写入数据、记录日志。这五步的顺序是固定的但每一步的具体实现可能因数据源和目标不同而不同。如果每个数据处理器都从头写这五步会有大量重复代码。用模板方法模式之后父类定义这五步的调用顺序子类只需要实现各自的数据读取、校验、转换和写入逻辑流程控制完全由父类负责。模板方法模式的关键在于“钩子方法”Hook Method。钩子方法是在父类中声明但实现为空的方法子类可以选择性地覆盖它。钩子方法给了子类更大的灵活性——子类可以决定是否要在某个步骤插入额外的逻辑而不是被迫实现所有步骤。比如在数据处理流程里父类可以在写入数据前后各留一个钩子子类如果需要做数据备份就在钩子里实现不需要就保持空实现。6.2 数据处理流程的模板方法实现下面用 Python 实现一个数据处理框架父类定义处理流程子类实现具体的数据读取和写入逻辑。from abc import ABC, abstractmethod class DataProcessor(ABC): def process(self): self.connect() data self.read_data() if self.validate(data): transformed self.transform(data) self.write_data(transformed) self.disconnect() self.log() def connect(self): print(建立连接) def disconnect(self): print(断开连接) def log(self): print(记录处理日志) abstractmethod def read_data(self): pass abstractmethod def transform(self, data): pass abstractmethod def write_data(self, data): pass def validate(self, data): return True class CsvProcessor(DataProcessor): def read_data(self): print(从 CSV 文件读取数据) return [row1, row2, row3] def transform(self, data): print(转换 CSV 数据) return [item.upper() for item in data] def write_data(self, data): print(f写入 CSV 数据: {data}) def validate(self, data): if not data: print(CSV 数据为空跳过处理) return False return True class JsonProcessor(DataProcessor): def read_data(self): print(从 JSON 接口读取数据) return {key: value} def transform(self, data): print(转换 JSON 数据) return {key: data[key].upper()} def write_data(self, data): print(f写入 JSON 数据: {data}) if __name__ __main__: csv_processor CsvProcessor() csv_processor.process() print(---) json_processor JsonProcessor() json_processor.process()这个实现里process方法是模板方法它定义了整个处理流程的骨架。connect、disconnect、log是具体方法所有子类共用。read_data、transform、write_data是抽象方法子类必须实现。validate是钩子方法默认返回 True子类可以覆盖它来实现自定义校验逻辑。CsvProcessor 覆盖了validate来检查数据是否为空JsonProcessor 则使用默认实现。6.3 模板方法模式的实战经验与注意事项模板方法模式在实际项目中有几个点需要特别注意。第一个是继承的局限性。模板方法模式依赖继承而继承是一种强耦合关系。如果父类的流程需要修改所有子类都会受到影响。所以在设计父类时流程的稳定性非常重要。我个人的经验是只有那些确实不会变的流程才适合用模板方法模式如果流程本身还在频繁调整那就不要急着抽象。第二个是抽象方法的粒度。抽象方法定义得太细子类实现起来会很繁琐定义得太粗子类又无法精确控制流程。我一般会把一个完整的业务步骤作为一个抽象方法而不是把步骤内部的每个小操作都拆成抽象方法。比如“读取数据”是一个抽象方法而不是把“打开文件”“读取内容”“关闭文件”都拆成抽象方法。这样既给了子类足够的实现自由度又不会让子类负担过重。第三个经验是关于钩子方法的使用。钩子方法不要定义太多否则子类会不知道哪些该覆盖哪些不该覆盖。我一般只在流程的关键节点留钩子比如“处理前”“处理后”“异常时”这几个位置。而且钩子方法的命名要清晰让人一看就知道是什么时机被调用的。比如beforeProcess、afterProcess、onError这样的命名就比hook1、hook2好得多。注意模板方法模式和策略模式都可以用来封装算法但它们的实现方式不同。模板方法用继承策略模式用组合。一般来说组合比继承更灵活所以如果能用策略模式就用策略模式。但模板方法模式在流程固定、步骤可变的场景下更直观因为它把流程控制权留在了父类子类只需要关心自己的步骤实现。7. 访问者模式在不改类的前提下增加新操作7.1 访问者模式的适用场景与双分派机制访问者模式表示一个作用于某对象结构中的各元素的操作它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。这个模式的核心是“双分派”——第一次分派是客户端调用访问者的visit方法第二次分派是元素对象调用访问者的具体方法把自身作为参数传进去。访问者模式是我认为所有行为型模式里最难理解的一个也是在实际项目中最容易被误用的一个。它的适用场景非常特定对象结构中的元素类很少变化但需要经常在这些元素上定义新的操作。比如一个编译器需要对着 AST抽象语法树做类型检查、代码生成、优化等多种操作AST 的节点类型相对稳定但操作会不断增加这就非常适合访问者模式。但如果元素类经常变化访问者模式就是灾难。因为每新增一个元素类所有访问者都要加一个对应的visit方法。我见过一个项目用访问者模式处理订单结果每加一种订单类型就要改五个访问者类维护成本极高。所以用访问者模式之前一定要想清楚元素类是否稳定操作是否经常增加如果答案是否那就别用。7.2 文档导出功能的访问者实现下面用 Java 实现一个文档导出功能。文档由段落和图片两种元素组成需要支持导出为 HTML 和 Markdown 两种格式。用访问者模式之后新增导出格式只需要加一个访问者类文档元素类完全不用改。interface DocumentElement { void accept(Visitor visitor); } class Paragraph implements DocumentElement { private String text; public Paragraph(String text) { this.text text; } public String getText() { return text; } Override public void accept(Visitor visitor) { visitor.visit(this); } } class Image implements DocumentElement { private String url; public Image(String url) { this.url url; } public String getUrl() { return url; } Override public void accept(Visitor visitor) { visitor.visit(this); } } interface Visitor { void visit(Paragraph paragraph); void visit(Image image); } class HtmlExportVisitor implements Visitor { Override public void visit(Paragraph paragraph) { System.out.println(p paragraph.getText() /p); } Override public void visit(Image image) { System.out.println(img src\ image.getUrl() \ /); } } class MarkdownExportVisitor implements Visitor { Override public void visit(Paragraph paragraph) { System.out.println(paragraph.getText()); } Override public void visit(Image image) { System.out.println(![]( image.getUrl() )); } } class Document { private java.util.ListDocumentElement elements new java.util.ArrayList(); public void addElement(DocumentElement element) { elements.add(element); } public void export(Visitor visitor) { for (DocumentElement element : elements) { element.accept(visitor); } } } public class Main { public static void main(String[] args) { Document doc new Document(); doc.addElement(new Paragraph(这是一段文字)); doc.addElement(new Image(https://example.com/img.png)); doc.addElement(new Paragraph(这是另一段文字)); System.out.println(--- HTML 导出 ---); doc.export(new HtmlExportVisitor()); System.out.println(--- Markdown 导出 ---); doc.export(new MarkdownExportVisitor()); } }这个实现里DocumentElement 是元素接口Paragraph 和 Image 是具体元素。Visitor 是访问者接口HtmlExportVisitor 和 MarkdownExportVisitor 是具体访问者。Document 是对象结构它持有一个元素列表export方法接收一个访问者并遍历所有元素。新增导出格式时只需要实现一个新的 Visitor 类Document、Paragraph、Image 都不用改。7.3 访问者模式的代价与替代方案访问者模式虽然强大但它的代价也不小。第一个代价是理解成本高。双分派机制对不熟悉的人来说很绕代码读起来需要跳来跳去。我团队里新来的同事第一次看访问者模式的代码时花了半天才理清调用关系。所以如果你的团队对设计模式不熟悉用访问者模式之前最好先做一次内部分享把原理讲清楚。第二个代价是扩展元素类困难。前面说过新增元素类要改所有访问者。如果你的元素类可能会增加那访问者模式就不合适。这时候可以考虑用策略模式加类型判断来替代虽然会有一些 if-else但扩展起来更简单。或者用反射机制来动态调用访问者方法避免修改访问者接口但反射会带来性能损耗和类型安全问题。第三个经验是关于访问者的状态管理。如果访问者需要在遍历过程中累积状态比如统计段落数量、收集所有图片链接那访问者对象就不能是单例的每次遍历都要新建一个访问者实例。我见过一个同事把访问者做成了单例结果多次导出时统计数据累加了排查了好久才发现问题。所以访问者如果是有状态的一定要每次遍历新建实例。模式适用场景扩展元素类扩展操作访问者模式元素类稳定、操作多变困难容易策略模式算法多变、客户端选择不涉及容易模板方法流程固定、步骤可变通过继承扩展需要改父类提示访问者模式在编译器、代码分析工具、报表生成系统里应用广泛。如果你在做类似的项目访问者模式值得深入学习。但如果你只是做普通的业务开发访问者模式的使用频率其实很低不必强行套用。8. 六个模式的横向对比与选型建议8.1 从变化维度看模式选型行为型模式的下半场六个模式每个都针对特定的变化维度。备忘录模式应对的是“状态需要回滚”的变化观察者模式应对的是“通知对象需要动态增减”的变化状态模式应对的是“对象行为随状态变化”的变化策略模式应对的是“算法需要灵活替换”的变化模板方法模式应对的是“流程固定但步骤可变”的变化访问者模式应对的是“操作需要在不改类的前提下增加”的变化。理解了这个对应关系选型就变得清晰了。当你发现代码里有一大坨 if-else 在判断状态时考虑状态模式当你发现每次新增一种算法都要改核心类时考虑策略模式当你发现多个对象需要同时感知某个变化时考虑观察者模式当你发现需要撤销功能时考虑备忘录模式当你发现多个类有相同的流程但步骤实现不同时考虑模板方法模式当你发现需要在不改类的前提下频繁增加操作时考虑访问者模式。但这里要强调一点模式不是越多越好。我见过一个项目把六个行为型模式全用上了结果代码复杂度爆炸新人根本看不懂。设计模式是为了降低复杂度而不是增加复杂度。如果一个简单的 if-else 就能解决的问题就不要硬套模式。模式的价值在于应对变化如果某个维度根本不会变那就别为它设计扩展点。8.2 常见组合使用场景在实际项目中这些模式经常组合使用。备忘录模式和命令模式组合可以实现撤销重做观察者模式和中介者模式组合可以实现事件总线状态模式和策略模式组合可以实现可配置的工作流引擎模板方法模式和策略模式组合可以实现可插拔的数据处理管道。我做过一个审批工作流系统就同时用到了状态模式、策略模式和模板方法模式。状态模式管理审批单的状态流转策略模式处理不同审批类型的审批规则模板方法模式定义审批流程的骨架。三个模式各司其职代码结构非常清晰。但我也要提醒一句组合使用模式时要注意职责边界不要让一个类同时承担多个模式的角色否则会变成“上帝类”。组合场景涉及模式解决的问题撤销重做备忘录命令操作可回滚事件总线观察者中介者多对多通知解耦工作流引擎状态策略状态流转规则可配置数据处理管道模板方法策略流程固定步骤可插拔编译器访问者解释器AST 遍历多操作8.3 我个人的模式使用心得最后分享几点我在实际项目中使用这些模式的心得。第一先写能跑的代码再重构出模式。不要一开始就想着用什么模式先把功能实现出来等发现代码有坏味道了再重构。我见过太多人一开始就过度设计结果需求一变设计全白费。第二模式的名字不重要重要的是理解它背后的思想。面试时能说出模式的名字当然好但实际工作中更重要的是知道什么时候该解耦、什么时候该封装、什么时候该留扩展点。这些思想比模式本身更有价值。第三多读优秀开源项目的源码。设计模式在书本上很抽象但在真实项目里你能看到它们是怎么被使用的。比如 Spring 框架里大量使用了模板方法模式和策略模式Java 的集合框架里使用了迭代器模式这些源码比任何教程都更有说服力。第四不要为了用模式而用模式。我职业生涯早期特别热衷于在项目里套模式觉得用得越多越显得专业。后来才明白最好的代码是简单直接的代码模式只是在你需要应对变化时的工具。如果变化还没来就不要提前为它设计。提示如果你正在准备面试建议把每个模式的“意图”“适用场景”“优缺点”和“一个真实例子”这四点准备好。面试官通常不会让你默写代码而是给你一个场景让你判断该用什么模式。所以理解场景比背诵定义更重要。这个系列写到这里行为型模式的十一个模式就全部覆盖完了。从上半场的责任链、命令、解释器、迭代器、中介者到下半场的备忘录、观察者、状态、策略、模板方法、访问者每一个模式都是前人经验的结晶。但记住模式是死的业务是活的。真正的高手不是能把所有模式倒背如流的人而是能根据具体场景选择最合适方案的人。我在实际项目里最常用的其实就三四个模式其他的更多是知道有这么个东西需要的时候能想起来去查。这就够了。

相关新闻

微信支付API v3接入实战:签名验签与回调处理全流程案例

微信支付API v3接入实战:签名验签与回调处理全流程案例

微信支付 API v3 的接入,是不少后端和全栈开发者在做交易类项目时绕不开的一道坎。相比早期的 v2 版本,v3 在安全模型上做了一次彻底的重构,签名机制、证书体系、回调验签逻辑全都换了新玩法。很多人第一次上手时,照着文档一步步走…

2026/10/15 11:52:41 阅读更多 →
Dash-iOS 中的 NSTimer+Blocks:让 NSTimer 直接使用 Block 回调的轻量级 Category 实践指南

Dash-iOS 中的 NSTimer+Blocks:让 NSTimer 直接使用 Block 回调的轻量级 Category 实践指南

开发工具 【免费下载链接】Dash-iOS Dash for iOS was discontinued. Please check out Dash for macOS instead. 项目地址: https://gitcode.com/gh_mirrors/da/Dash-iOS 点击查看 免费下载 导读 本文围绕 Dash-iOS 仓库中引入的第三方组件 NSTimer-Blocks&#…

2026/10/15 11:52:41 阅读更多 →
Qt QSplitter核心原理与工程实践:解决面板拖拽与布局恢复

Qt QSplitter核心原理与工程实践:解决面板拖拽与布局恢复

1. 为什么需要分割器:布局管理器解决不了的面板大小分配做了几年Qt桌面应用,我越来越觉得“布局”这件事被很多人低估了。刚开始写界面时,我也喜欢用QHBoxLayout和QVBoxLayout把控件一排排摆整齐,觉得能自动伸缩就很满足了。直到某…

2026/10/15 11:52:41 阅读更多 →

最新新闻

Vue+ThinkPHP-Laravel构建二手车交易平台实战

Vue+ThinkPHP-Laravel构建二手车交易平台实战

1. 项目背景与核心价值 二手汽车交易市场近年来呈现爆发式增长态势,但传统线下交易模式存在信息不对称、交易流程繁琐、信任成本高等痛点。基于VueThinkPHP-Laravel的混合架构方案,恰好能解决行业数字化转型中的三个关键问题: 前端体验优化 …

2026/10/15 12:38:06 阅读更多 →
双端队列deque全解:从底层原理到单调队列与竞赛应用

双端队列deque全解:从底层原理到单调队列与竞赛应用

做算法题的人大概都经历过这样的场景:一道滑动窗口最大值的题,第一反应是priority_queue,塞进去跑一遍O(n log n)也AC了,但看着题解区那个O(n)的写法总觉得自己绕了远路。再过一阵子遇到DP优化,状态转移里带着一个&quo…

2026/10/15 12:38:06 阅读更多 →
【OpenClaw 本地实战 Ep.1】抛弃 Ollama?转向 LM Studio!Windows 下用 NVIDIA 显卡搭建 OpenClaw 本地极速推理服务

【OpenClaw 本地实战 Ep.1】抛弃 Ollama?转向 LM Studio!Windows 下用 NVIDIA 显卡搭建 OpenClaw 本地极速推理服务

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 12:38:06 阅读更多 →
八数码问题C语言BFS实现:从伪广搜到工业级最短路径求解

八数码问题C语言BFS实现:从伪广搜到工业级最短路径求解

简介:本资源是一份面向计算机专业本科生与算法初学者的八数码问题求解实践材料,聚焦宽度优先搜索(BFS)思想在经典状态空间搜索问题中的C语言实现。资源以详实的DOC文档形式呈现,完整包含算法原理剖析、结构体设计逻辑、…

2026/10/15 12:38:06 阅读更多 →
OV7670+SDRAM+VGA:FPGA实时图像链路设计与避坑指南

OV7670+SDRAM+VGA:FPGA实时图像链路设计与避坑指南

简介:这是一套面向FPGA与数字图像处理学习者的完整工程实践资料,围绕OV7670 CMOS摄像头、SDRAM缓存与VGA显示构建实时视频采集与显示链路,并集成中值滤波算法改善画质。项目覆盖IIC寄存器配置、SDRAM控制器读写、VGA时序驱动以及灰度图像去噪…

2026/10/15 12:38:06 阅读更多 →
从8k到1M上下文的通用化:利用Qwen-Agent的创新实践

从8k到1M上下文的通用化:利用Qwen-Agent的创新实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 12:37:05 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →