1. 这不是教科书里的“行为型模式”,而是我在三年高并发系统重构中亲手拆解、验证、踩坑后的真实复盘
你搜“设计模式06——行为型模式”,大概率会看到一张表格:策略、观察者、命令、责任链、状态、模板方法、访问者、中介者、备忘录……然后是定义、UML图、Java代码片段。我当年也这么学,直到在电商大促秒杀系统里,因为硬套“观察者模式”监听库存变更,结果消息队列积压200万条,服务雪崩,凌晨三点被电话叫醒重启集群。那一刻我才明白:行为型模式不是让你背概念的,它是解决对象之间如何协作、如何传递控制权、如何让变化局部化的一套实战工具箱。它不讲“应该怎么做”,只回答“当系统出现XX症状时,哪种协作结构能最干净地切开耦合”。比如用户下单后要触发风控校验、积分更新、物流预占、短信通知——这四个动作谁先谁后?谁失败了要回滚?谁可以异步?谁必须强一致?这些不是靠if-else堆出来的,而是用行为型模式的骨架提前搭好的。它真正价值在于:当你面对一个“流程变来变去”“规则频繁增删”“不同角色间推来推去”的业务场景时,能立刻识别出这是“状态模式”的典型征兆,或是“责任链”该上场了。本文不讲UML,不贴标准代码,只还原我在支付网关、风控引擎、订单履约三个核心系统里,如何用行为型模式把原本300行if-else的调度逻辑,压缩成80行可插拔、可配置、可单测的协作结构。适合正在写大作业却卡在“策略模式怎么和Spring整合”的同学,也适合被线上Bug追着跑、发现“改个校验规则就要动五个类”的资深工程师。你不需要记住所有模式名字,但得知道:当你的代码开始出现“条件分支爆炸”“调用链越来越长”“改一处牵动全局”时,行为型模式就是你手边最锋利的解耦刀。
2. 行为型模式的本质:不是设计“对象”,而是设计“对象之间的契约”
2.1 为什么说行为型模式是“协作协议”的设计语言?
很多人误以为设计模式是“造轮子”,其实恰恰相反——它是避免重复造轮子的协议说明书。举个真实例子:我们做风控引擎时,需要支持“实名认证”“反欺诈评分”“黑名单拦截”“额度校验”四类规则,每类规则由不同团队维护,上线节奏不同。最初用if-else硬编码:
if (user.isRealName()) { if (!fraudService.score(user) > 0.8) { if (!blackListService.contains(user.getId())) { if (creditService.check(user.getId(), amount)) { // 放行 } else { // 额度不足 } } else { // 黑名单 } } else { // 反欺诈失败 } } else { // 未实名 }问题立刻暴露:新增“设备指纹校验”规则?得在每个if里加一层嵌套;想把“黑名单拦截”改成异步?得重写整个调用链;某规则临时下线?得注释掉对应if块,极易漏掉回滚逻辑。这不是代码质量问题,是协作契约缺失——没人约定“规则执行失败时,后续规则是否继续?”“规则结果如何聚合?”“谁负责兜底?”行为型模式就是为这类问题提供标准化契约。比如责任链模式,它强制定义了三个契约要素:
- 统一入口:所有规则实现同一个
RuleHandler接口,handle(Request request)方法; - 链式传递:每个处理器持有下一个处理器引用,
next.handle(request); - 终止协议:处理器返回
true表示处理完成(如拦截),返回false表示继续传递。
这个契约比任何文档都管用。新来的同事只要看RuleHandler接口,就知道他写的规则必须遵守“输入Request、输出boolean、失败时调用next”,不用再问“我该return true还是throw exception”。
2.2 五种高频行为型模式的核心契约对比
| 模式名称 | 解决的核心协作问题 | 关键契约要素 | 典型适用场景 | 我踩过的坑 |
|---|---|---|---|---|
| 策略模式 | 同一行为有多种算法实现,需运行时切换 | 定义策略接口+具体策略类+上下文持有策略引用 | 支付渠道选择(微信/支付宝/银联)、折扣计算(满减/折上折/会员价) | 初期没做策略工厂,每次新增渠道都要改上下文switch,后来用Spring BeanName自动注册解决 |
| 观察者模式 | 一个对象状态改变,需通知多个依赖对象 | 主题(Subject)维护观察者列表+注册/移除/通知方法;观察者(Observer)实现update() | 订单状态变更推送(短信/邮件/APP推送)、库存扣减后通知缓存更新 | 直接new Observer导致内存泄漏,后来改用WeakReference+事件总线解耦 |
| 命令模式 | 请求发送者与接收者解耦,支持撤销/重做/日志 | 命令接口(execute()/undo())+具体命令类(封装接收者+动作)+调用者(Invoker) | 编辑器操作(复制/粘贴/撤销)、分布式事务补偿(TCC中的Try/Confirm/Cancel) | undo()没考虑幂等性,网络重试导致数据错乱,后来加唯一事务ID校验 |
| 状态模式 | 对象行为随内部状态改变而改变,避免大量条件分支 | 状态接口(handle())+具体状态类(封装状态转移逻辑)+上下文持有当前状态引用 | 订单生命周期(待支付→已支付→发货中→已完成→已取消)、审批流程(草稿→提交→审核中→通过→驳回) | 状态转移逻辑散落在各状态类中,难以全局查看流转图,后来用状态机DSL生成可视化流程图 |
| 模板方法模式 | 封装固定流程,允许子类重写部分步骤 | 抽象类定义模板方法(final)+抽象方法(子类实现)+钩子方法(可选覆盖) | 数据导入(读取→校验→转换→保存→通知)、HTTP请求处理(解析→鉴权→业务处理→响应) | 子类重写钩子方法时,忘了调用父类super.xxx(),导致前置校验失效,加了单元测试强制覆盖 |
提示:别死记模式名字,先诊断你的代码有没有“条件分支爆炸”(状态模式)、“调用链过长且易变”(责任链)、“同一功能多套算法”(策略)。契约比UML图重要十倍——它决定了团队协作的底线。
2.3 行为型模式的“危险区”:什么时候不该用?
模式是药,不是补品。我见过太多项目为用而用,结果更糟。三个明确信号,说明你该停手:
- 对象数量少于3个:比如只有“用户”和“订单”两个对象,硬套观察者模式,反而增加一层不必要的间接。行为型模式的价值在于管理复杂协作关系,简单场景用直接调用更清晰。
- 协作逻辑高度稳定:如果“支付成功后发短信”这个流程未来三年都不会变,写个
paymentService.sendSms()就行。模式解决的是变化点,没有变化点,模式就是累赘。 - 性能敏感且调用频次极高:比如高频交易系统的行情推送,观察者模式的循环通知可能引入毫秒级延迟。这时用发布-订阅(Pub/Sub)中间件或直接回调更合适。模式是为可维护性服务的,不是为性能优化的。
3. 实战拆解:用状态模式重构订单系统,把300行if-else变成可配置的状态机
3.1 重构前的“地狱代码”长什么样?
这是我们订单履约服务的真实代码(已脱敏):
public class OrderService { public void processOrder(Order order) { switch (order.getStatus()) { case "CREATED": if (inventoryService.lock(order.getItems())) { order.setStatus("PAID"); paymentService.charge(order); // 发送支付成功通知 notifyService.send("PAY_SUCCESS", order); } else { order.setStatus("OUT_OF_STOCK"); notifyService.send("OUT_OF_STOCK", order); } break; case "PAID": if (logisticsService.reserve(order.getDeliveryAddress())) { order.setStatus("SHIPPED"); // 更新物流单号 order.setTrackingNo(logisticsService.createTracking()); notifyService.send("SHIPPED", order); } else { order.setStatus("LOGISTICS_FAILED"); notifyService.send("LOGISTICS_FAILED", order); } break; case "SHIPPED": // 用户确认收货 if (order.getReceivedTime() != null) { order.setStatus("COMPLETED"); // 发放积分 pointService.add(order.getUserId(), order.getAmount()); notifyService.send("COMPLETED", order); } else if (System.currentTimeMillis() - order.getShippedTime() > 7 * 24 * 3600 * 1000L) { // 超时自动确认 order.setStatus("COMPLETED"); pointService.add(order.getUserId(), order.getAmount()); notifyService.send("AUTO_COMPLETED", order); } break; case "CANCELLED": // 已取消订单,不做任何处理 break; default: throw new IllegalStateException("Unknown status: " + order.getStatus()); } } }问题显而易见:
- 状态判断分散:状态检查在
switch里,状态变更在if里,状态含义在字符串常量里,三处不一致; - 逻辑混杂:库存锁定、支付、物流、通知、积分全部挤在一个方法里,改一个功能要通读全篇;
- 扩展困难:想加“部分发货”状态?得在
switch里加case,在每个相关分支里补逻辑,极易遗漏; - 测试噩梦:每个状态分支都要单独构造测试数据,覆盖率难保证。
3.2 状态模式重构:从“状态驱动”到“状态即对象”
核心思路:把每个状态变成一个独立对象,状态转移逻辑封装在状态对象内部。这样,Order对象不再关心“下一步该做什么”,只负责“当前状态是什么”和“触发什么事件”。
第一步:定义状态接口
// 所有状态必须实现此接口 public interface OrderState { // 处理支付事件 void handlePayment(OrderContext context); // 处理发货事件 void handleShipment(OrderContext context); // 处理收货事件 void handleReceipt(OrderContext context); // 处理取消事件 void handleCancellation(OrderContext context); // 获取状态码(用于持久化) String getStateCode(); }第二步:实现具体状态类(以“已支付”状态为例)
// PAID状态:已支付,等待发货 public class PaidState implements OrderState { @Override public void handlePayment(OrderContext context) { // 已支付状态下再次支付?忽略或报错 throw new IllegalStateException("Order is already paid"); } @Override public void handleShipment(OrderContext context) { // 尝试锁定物流资源 if (context.getLogisticsService().reserve(context.getOrder().getDeliveryAddress())) { // 成功:更新订单,切换到SHIPPED状态 context.getOrder().setTrackingNo( context.getLogisticsService().createTracking() ); context.changeState(new ShippedState()); // 关键!状态切换 context.getNotifyService().send("SHIPPED", context.getOrder()); } else { // 失败:切换到LOGISTICS_FAILED状态 context.changeState(new LogisticsFailedState()); context.getNotifyService().send("LOGISTICS_FAILED", context.getOrder()); } } @Override public void handleReceipt(OrderContext context) { // 已支付未发货,不能收货 throw new IllegalStateException("Cannot receive before shipment"); } @Override public void handleCancellation(OrderContext context) { // 已支付可取消,触发退款 context.getPaymentService().refund(context.getOrder()); context.changeState(new CancelledState()); context.getNotifyService().send("CANCELLED", context.getOrder()); } @Override public String getStateCode() { return "PAID"; } }第三步:创建上下文(OrderContext)管理状态
// 上下文:持有订单和当前状态,提供状态切换能力 public class OrderContext { private Order order; private OrderState currentState; // 构造时注入初始状态 public OrderContext(Order order, OrderState initialState) { this.order = order; this.currentState = initialState; } // 供外部调用的事件入口(对调用方屏蔽状态细节) public void onPayment() { currentState.handlePayment(this); } public void onShipment() { currentState.handleShipment(this); } public void onReceipt() { currentState.handleReceipt(this); } public void onCancel() { currentState.handleCancellation(this); } // 状态切换方法(关键!) public void changeState(OrderState newState) { this.currentState = newState; // 持久化状态到数据库 order.setStatus(newState.getStateCode()); orderRepository.save(order); } // Getter供状态对象使用 public Order getOrder() { return order; } public PaymentService getPaymentService() { return paymentService; } public LogisticsService getLogisticsService() { return logisticsService; } public NotifyService getNotifyService() { return notifyService; } // ...其他服务 }第四步:初始化与使用
// 创建订单时,根据初始状态选择对应状态对象 Order order = new Order(...); OrderContext context = new OrderContext( order, new CreatedState() // 初始状态 ); // 外部调用变得极其简洁 context.onPayment(); // 触发支付事件,内部自动处理状态转移 context.onShipment(); // 触发发货事件3.3 重构后的收益与关键细节
- 代码可读性提升:
OrderContext类只剩状态切换逻辑,不到50行;每个状态类专注自身职责,PaidState类只处理“已支付”状态下的所有可能事件。 - 扩展性极强:新增“部分发货”状态?只需新建
PartiallyShippedState类,实现对应事件方法,修改ShippedState中handleReceipt()逻辑即可,完全不影响其他状态。 - 测试成本降低:每个状态类可独立单元测试。例如测试
PaidState.handleShipment(),只需MockLogisticsService返回true/false,验证状态切换和通知是否正确。 - 状态流转可视化:所有状态转移都在
changeState()调用中,配合日志可轻松绘制状态流转图。
注意:状态对象必须是无状态的(Stateless),它只封装行为逻辑,不持有订单数据。所有数据通过
OrderContext传入。否则状态对象会变成“胖对象”,违背模式初衷。
4. 行为型模式落地避坑指南:那些文档里不会写的血泪教训
4.1 策略模式:Spring Boot中如何避免“策略爆炸”?
策略模式最大的坑不是不会写,而是策略越来越多,配置越来越乱。我们曾有个促销系统,支持12种优惠券类型,每种对应一个策略类。初期用@Service("couponStrategy_1")标注,然后在CouponService里用ApplicationContext.getBean("couponStrategy_" + type)获取。结果:
- 新增策略要手动改
getBean()字符串,容易拼错; - 启动时所有策略类都被加载,哪怕90%用不到;
- 策略参数(如满减门槛)硬编码在类里,无法动态配置。
解决方案:用Spring的ListableBeanFactory自动装配+策略工厂
@Component public class CouponStrategyFactory { private final Map<String, CouponStrategy> strategyMap; // 构造器注入所有CouponStrategy实现类 public CouponStrategyFactory(List<CouponStrategy> strategies) { this.strategyMap = strategies.stream() .collect(Collectors.toMap( CouponStrategy::getType, // 每个策略实现自己的getType()方法,返回"FULL_REDUCTION" Function.identity() )); } public CouponStrategy getStrategy(String type) { CouponStrategy strategy = strategyMap.get(type); if (strategy == null) { throw new IllegalArgumentException("No strategy found for type: " + type); } return strategy; } } // 策略接口 public interface CouponStrategy { String getType(); // 返回策略类型标识 BigDecimal calculateDiscount(Order order, Coupon coupon); } // 具体策略(自动被Spring扫描) @Service public class FullReductionStrategy implements CouponStrategy { @Value("${coupon.full-reduction.threshold:100}") // 参数外置化 private BigDecimal threshold; @Override public String getType() { return "FULL_REDUCTION"; } @Override public BigDecimal calculateDiscount(Order order, Coupon coupon) { if (order.getTotalAmount().compareTo(threshold) >= 0) { return coupon.getDiscountAmount(); } return BigDecimal.ZERO; } }实操心得:策略类必须实现getType()方法,这是工厂识别的关键。不要用枚举值硬编码,用字符串更灵活(支持运行时动态加载)。
4.2 观察者模式:如何防止内存泄漏和事件丢失?
观察者模式在Web应用中最常见的错误是观察者对象生命周期管理不当。比如在Controller里new一个OrderObserver,注册到订单服务,但Controller实例销毁后,观察者还在静态列表里,导致内存泄漏。
正确做法:用弱引用(WeakReference)+事件总线(EventBus)
@Component public class EventBus { // 使用ConcurrentHashMap存储观察者,Key为事件类型,Value为WeakReference列表 private final Map<Class<?>, CopyOnWriteArrayList<WeakReference<EventListener>>> listeners = new ConcurrentHashMap<>(); public <T> void register(Class<T> eventType, EventListener<T> listener) { listeners.computeIfAbsent(eventType, k -> new CopyOnWriteArrayList<>()) .add(new WeakReference<>(listener)); } public <T> void post(T event) { Class<?> eventType = event.getClass(); List<WeakReference<EventListener>> refs = listeners.get(eventType); if (refs != null) { // 过滤掉已被GC的观察者 refs.removeIf(ref -> ref.get() == null); refs.forEach(ref -> { EventListener listener = ref.get(); if (listener != null) { listener.onEvent(event); } }); } } } // 观察者接口 public interface EventListener<T> { void onEvent(T event); } // 使用示例 @Service public class SmsNotificationService implements EventListener<OrderPaidEvent> { @PostConstruct public void init() { eventBus.register(OrderPaidEvent.class, this); } @Override public void onEvent(OrderPaidEvent event) { smsService.send(event.getOrder().getPhone(), "支付成功!"); } }提示:WeakReference确保观察者被GC后自动清理,避免内存泄漏;
CopyOnWriteArrayList保证并发安全;@PostConstruct确保Spring容器启动后注册,避免空指针。
4.3 命令模式:分布式环境下如何保证命令执行的幂等性?
命令模式在本地很好用,但放到微服务架构里,网络超时会导致命令重复提交。比如用户点击“确认收货”,前端发起请求,网关超时返回失败,但后端其实已执行成功,用户再次点击,就重复发放积分。
解决方案:命令ID + 幂等表
// 命令基类,所有命令必须有唯一ID public abstract class Command { private final String commandId; // UUID生成,前端传入 private final long timestamp; // 时间戳,用于过期判断 public Command(String commandId) { this.commandId = commandId; this.timestamp = System.currentTimeMillis(); } public String getCommandId() { return commandId; } public long getTimestamp() { return timestamp; } // 抽象执行方法 public abstract void execute(); } // 幂等服务 @Service public class IdempotentService { @Transactional public boolean executeIfNotExists(Command command, Supplier<Boolean> action) { // 先查幂等表 IdempotentRecord record = idempotentRepository.findById(command.getCommandId()).orElse(null); if (record != null) { // 已存在,直接返回结果(假设记录里存了上次执行结果) return record.isSuccess(); } // 执行业务逻辑 boolean result = action.get(); // 记录幂等结果 idempotentRepository.save(IdempotentRecord.builder() .commandId(command.getCommandId()) .success(result) .timestamp(command.getTimestamp()) .build()); return result; } } // 具体命令 public class ConfirmReceiptCommand extends Command { private final Long orderId; public ConfirmReceiptCommand(String commandId, Long orderId) { super(commandId); this.orderId = orderId; } @Override public void execute() { idempotentService.executeIfNotExists(this, () -> { // 核心业务逻辑:更新订单状态、发放积分 orderService.confirmReceipt(orderId); pointService.addPoints(orderId); return true; }); } }关键点:命令ID由前端生成并传递,确保同一次用户操作ID唯一;幂等表用commandId作为主键,天然防重复插入;executeIfNotExists方法将幂等逻辑与业务逻辑解耦。
5. 行为型模式进阶:当单一模式不够用时,如何组合使用?
5.1 策略+状态:动态切换状态机的执行策略
订单状态机里,“已支付”状态下的发货逻辑,可能因商品类型不同而不同:普通商品走快递,虚拟商品走电子交付,生鲜商品走冷链。这时,PaidState.handleShipment()不能写死物流逻辑,而应委托给策略。
public class PaidState implements OrderState { // 注入策略工厂 private final LogisticsStrategyFactory strategyFactory; public PaidState(LogisticsStrategyFactory strategyFactory) { this.strategyFactory = strategyFactory; } @Override public void handleShipment(OrderContext context) { Order order = context.getOrder(); // 根据商品类型选择物流策略 LogisticsStrategy strategy = strategyFactory.getStrategy(order.getGoodsType()); // 策略执行发货 strategy.ship(order); // 状态切换 context.changeState(new ShippedState()); } }优势:状态机负责“何时切换”,策略负责“如何执行”,职责彻底分离。新增商品类型?只需新增策略类,状态机代码零修改。
5.2 观察者+命令:构建可追溯的事件驱动架构
在风控系统中,我们希望所有规则执行结果都记录下来,便于审计。观察者模式负责通知,命令模式负责执行,两者结合:
// 事件:规则执行完成 public class RuleExecutionEvent { private final String ruleId; private final boolean passed; private final String reason; public RuleExecutionEvent(String ruleId, boolean passed, String reason) { this.ruleId = ruleId; this.passed = passed; this.reason = reason; } } // 观察者:记录审计日志 @Component public class AuditLogObserver implements EventListener<RuleExecutionEvent> { @Override public void onEvent(RuleExecutionEvent event) { auditLogService.record( event.getRuleId(), event.isPassed(), event.getReason(), ThreadLocalContext.getCurrentUserId() // 上下文用户 ); } } // 观察者:触发补偿命令(如规则失败需回滚) @Component public class CompensationObserver implements EventListener<RuleExecutionEvent> { @Override public void onEvent(RuleExecutionEvent event) { if (!event.isPassed()) { // 创建补偿命令 CompensationCommand command = new CompensationCommand( event.getRuleId(), ThreadLocalContext.getCurrentOrderId() ); // 异步执行 compensationExecutor.submit(command); } } }效果:一个事件(RuleExecutionEvent)触发多个观察者,各自执行不同职责(日志、补偿、告警),完全解耦。新增审计需求?加个新观察者就行。
5.3 责任链+模板方法:打造可插拔的校验流水线
登录校验需要依次执行:账号密码校验、验证码校验、风控评分、IP黑名单校验。每个校验环节都需“前置准备→执行校验→后置清理”,但具体逻辑不同。
// 模板方法定义校验骨架 public abstract class ValidationHandler<T> { // 模板方法:定义校验流程 public final ValidationResult validate(T context) { try { preValidate(context); // 模板方法,子类可覆盖 ValidationResult result = doValidate(context); postValidate(context, result); // 模板方法,子类可覆盖 return result; } catch (Exception e) { return ValidationResult.fail("系统异常: " + e.getMessage()); } } protected void preValidate(T context) {} protected void postValidate(T context, ValidationResult result) {} // 抽象方法:子类必须实现 protected abstract ValidationResult doValidate(T context); } // 具体校验器(链中一环) public class RiskScoreValidationHandler extends ValidationHandler<LoginContext> { @Override protected ValidationResult doValidate(LoginContext context) { double score = riskService.calculateScore(context.getIp(), context.getUserId()); if (score > 0.9) { return ValidationResult.fail("风控评分过高,拒绝登录"); } return ValidationResult.success(); } } // 责任链组装 public class LoginValidatorChain { private final List<ValidationHandler<LoginContext>> handlers; public LoginValidatorChain(List<ValidationHandler<LoginContext>> handlers) { this.handlers = handlers; } public ValidationResult validate(LoginContext context) { for (ValidationHandler<LoginContext> handler : handlers) { ValidationResult result = handler.validate(context); if (!result.isSuccess()) { return result; // 任一环节失败,立即返回 } } return ValidationResult.success(); } }组合威力:责任链管理“执行顺序”,模板方法管理“执行结构”,两者结合,既保证流程可控,又保证每个环节结构统一。
6. 最后一点实在话:别为了模式而模式,先让代码能跑起来
我带过不少实习生,他们一上来就想把所有if-else都替换成策略模式,结果写了20个策略类,最后发现80%的策略永远用不上。行为型模式不是银弹,它是应对复杂性的手术刀,不是装饰代码的金粉。我的建议很朴素:
- 先写能跑的代码:用最直白的if-else把功能做出来,跑通流程,覆盖核心场景。
- 再找“痛点”:当出现“改一个地方要动七八个文件”“新加一个规则要复制粘贴一堆代码”“测试用例写不完”时,这就是模式该登场的信号。
- 从小处切入:别一上来重构整个订单系统,先挑一个最痛的模块(比如支付回调处理),用状态模式重写,验证效果。
- 模式是手段,不是目标:最终目标是让代码易读、易改、易测。如果用了模式后,新人看不懂、改起来更费劲、测试更难写,那一定是用错了。
我在支付网关项目里,用状态模式重构后,新同事第一天就能独立修改“已支付”状态的发货逻辑,因为所有相关代码都在PaidState.java里,不用在几千行代码里grep。这才是模式真正的价值——它让知识沉淀在代码里,而不是在某个老员工的脑子里。