使用命令模式有哪些常见误区?
简化版
命令模式常见误区包括:把所有方法调用都包装成命令、把业务逻辑全塞进命令类、以为 undo() 一定能简单反向执行、忽略异步命令的幂等和失败处理、命令类过多但没有统一管理价值。命令模式要用在请求确实需要被管理的地方。
详细版
常见误区有:
- 过度设计:普通 CRUD 也写命令类;
- 职责错位:命令类承载全部业务逻辑,Receiver 形同虚设;
- 撤销误解:认为所有操作都能简单
undo(); - 忽略失败:
execute()失败后历史栈或状态没有处理; - 忽略幂等:异步命令重复执行导致数据错误;
- 忽略持久化:内存命令队列重启即丢;
- 命令参数过大:命令对象持有复杂上下文,难以序列化和排查;
- 没有边界:命令、策略、责任链混用但意图不清。
面试时可以强调:命令模式的收益来自请求对象化,如果请求不需要排队、撤销、记录、调度或组合,就不一定需要它。
完整版教学
一、误区一:所有调用都写成命令
有些人学完命令模式后,会把所有 Service 调用都改成:
GetUserCommand
UpdateUserCommand
ListUserCommand
如果这些命令只是简单调用一行 service,没有撤销、队列、日志、调度等需求,就只是增加样板代码。
设计模式不是越多越好,能解决问题才有价值。
二、误区二:命令类变成大业务类
命令对象应该封装请求和调度逻辑,但不应该把所有业务细节都塞进去。
不好的写法:
CancelOrderCommand 里直接写库存、支付、优惠券、消息、日志全部逻辑
更清晰的做法是:
CancelOrderCommand 调用 OrderService.cancel()
OrderService 处理核心业务
命令负责表达“执行取消订单请求”,业务服务负责真正的领域规则。
三、误区三:把撤销想得太简单
本地编辑器里的撤销很直观,插入文字就删除文字。
但业务系统里的撤销常常不是反向调用:
- 支付不能随便反向打款;
- 发货不能简单取消;
- 短信无法撤回;
- 外部系统调用可能已经产生副作用。
这类场景需要补偿、冲正、状态机或人工审核,而不是简单 undo()。
四、误区四:异步命令不做幂等
命令进入消息队列后,可能被重复消费。
如果没有幂等控制:
发券命令重复执行 -> 用户多拿券
扣库存命令重复执行 -> 库存被多扣
发送通知命令重复执行 -> 用户收到多条消息
所以异步命令要设计:
- commandId;
- 业务唯一键;
- 去重表;
- 唯一索引;
- 执行状态机。
五、误区五:只用内存队列却期待可靠性
本地内存队列简单易用,但进程重启就丢。
如果任务不能丢,应该考虑:
- 数据库任务表;
- 消息队列;
- 定时扫描补偿;
- 执行状态持久化;
- 死信队列。
命令模式不是可靠队列,它只是任务抽象。
六、误区六:命令参数持有太多对象
命令对象如果持有大量上下文,比如 HTTP Request、数据库连接、线程池、Spring Bean,会导致:
- 难以序列化;
- 生命周期混乱;
- 内存泄漏风险;
- 排查困难。
更好的方式是命令只保存必要参数,执行时通过 Handler 或 Service 获取依赖。
七、误区七:和其他模式混在一起但说不清目的
命令模式、策略模式、责任链模式都可能出现接口和实现类。
判断时要看目的:
- 请求要管理:命令;
- 算法要替换:策略;
- 请求要沿链处理:责任链。
如果团队说不清为什么用命令模式,后续维护会很痛苦。
八、常见误区与追问
判断是否误用可以做一次“删除命令层”实验:若删掉后仍没有损失撤销、排队、日志或延迟执行能力,这层多半只是转发。假设10个简单 CRUD 方法被包装成10个只调用一行 Service 的命令类,维护成本增加却没有获得请求对象化的收益。真正需要命令模式时,命令自身应成为可调度、可记录或可补偿的业务载体。
| 检查维度 | 判定依据 |
|---|---|
| 只有同步转发 | 优先直接调用服务 |
| 需要排队或撤销 | 命令对象提供明确收益 |
心法:先找“请求成为对象后要做什么”,再决定是否引入 Command。
- 误区:类名带 Command 就使用了命令模式。 模式取决于请求是否被对象化并与触发者解耦,而不是命名。
- 追问:命令类数量膨胀怎么控制? 按业务动作建模,复用参数模型,并避免为没有调度价值的每个方法机械建类。
- 误区:撤销就是调用相反方法。 相反动作需要执行前状态,且外部副作用未必可逆。
- 追问:命令失败后一定要自动重试吗? 只有瞬时错误且命令幂等时适合自动重试,校验失败应直接终止。
- 追问:怎样识别命令对象过重? 若它持有连接、容器上下文或大量可变实体,序列化、重试和测试都会变得困难。
九、加强记忆
命令模式最容易误用在两个地方:简单调用硬包装、复杂业务全塞进命令。正确做法是只在请求需要撤销、排队、日志、组合、异步或补偿时使用它,并把业务规则留在合适的服务或领域对象中。