写在前面
收官篇。讲两个行为型模式——命令和状态。选它俩收尾,是因为它们正好接上 DDD 系列的两个核心:命令模式 = CQRS 里的 Command 对象(DDD 第五篇);状态模式 = 订单聚合里的状态流转(DDD 第二篇)。
讲完这篇,设计模式系列和 DDD 系列就完美合龙了。
一、命令模式:把"请求"变成对象
1
2
3
4
5
6
7
8
9
10
11
12
| 场景:一个编辑器,要做"加粗""复制""粘贴"等操作,还要支持【撤销/重做】。
朴素写法(直接调方法):
editor.Bold(); editor.Copy(); editor.Paste();
问题:
✗ 没法撤销(不知道"刚才做了什么")
✗ 没法排队、记录日志、宏录制
✗ 调用者(按钮/快捷键)和具体操作绑死
反转:把"做一件事"封装成一个对象,里面记录"做什么 + 怎么撤"。
调用者只管"执行这个对象",不知道里面是什么操作。
→ 这就是命令模式。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| 意图:把请求封装成对象,从而可以参数化、排队、记录、撤销。
结构:
ICommand ← 命令接口(Execute / Undo)
ConcreteCommand ← 每个操作一个命令对象(持有接收者)
Invoker ← 调用者(按钮、队列),触发命令
Receiver ← 真正干活的(Editor、Service)
价值:
✓ 撤销/重做(每个命令记录"怎么撤")
✓ 队列/调度/延迟执行(命令是对象,可存可排)
✓ 宏/日志/重放(命令序列化、记录、批量)
✓ 调用者与接收者解耦
|
二、命令模式手写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // 命令接口:执行 + 撤销
public interface ICommand
{
void Execute();
void Undo();
}
// 具体命令:加粗(持有接收者 Editor)
public class BoldCommand(Editor _editor) : ICommand
{
private string? _previousText; // 记录撤销所需的状态
public void Execute()
{
_previousText = _editor.Text;
_editor.BoldSelection();
}
public void Undo() => _editor.Text = _previousText!;
}
// 调用者:命令历史栈,支持撤销
public class CommandHistory
{
private readonly Stack<ICommand> _done = new();
public void Execute(ICommand cmd) { cmd.Execute(); _done.Push(cmd); }
public void Undo() { if (_done.Count > 0) _done.Pop().Undo(); }
}
|
1
2
3
4
| 关键:每个命令对象封装了"做什么 + 怎么撤",
调用者(历史栈)只管 Push/Pop/Execute,不关心具体操作。
→ 加新操作 = 加一个命令类,调用者不动(开闭)。
→ 撤销/重做、命令队列、宏录制,全靠"命令是对象"这一点。
|
三、.NET 里的命令模式:CQRS 的 Command
我在 DDD 第五篇讲 CQRS 时说过:写侧的入口是"命令"(CancelOrderCommand)。那正是命令模式的工程化应用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // CQRS 命令对象(MediatR 的 IRequest)
public sealed record CancelOrderCommand(Guid OrderId, string Reason) : IRequest;
// 命令处理器(接收者)
public class CancelOrderHandler(IOrderRepository _repo) : IRequestHandler<CancelOrderCommand>
{
public async Task Handle(CancelOrderCommand cmd, CancellationToken ct)
{
var order = await _repo.Find(cmd.OrderId, ct) ?? throw new NotFoundException();
order.Cancel(cmd.Reason);
await _repo.Save(order, ct);
}
}
// 调用者:发命令,不关心谁处理
await _mediator.Send(new CancelOrderCommand(orderId, "误操作"));
|
1
2
3
4
5
6
7
8
9
10
| 对应关系:
ICommand(Execute) → CancelOrderCommand(数据载体)+ Handler(逻辑)
Invoker → MediatR(_mediator.Send 派发)
Receiver → Order 聚合 / 仓储
CQRS 把"命令"提升为一等公民:
✓ 可序列化、可入队(消息队列)、可重放
✓ 写侧与读侧、调用方与处理方解耦
✓ 配合事件溯源,命令/事件可审计
→ 这就是命令模式在现代架构里的"高阶用法"。
|
四、状态模式:行为随状态变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| 场景:订单有 草稿/已下单/已支付/已发货/已取消 多种状态,
每种状态下,"能不能改地址""能不能取消""能不能再付款"的规则都不一样。
朴素写法(每个方法里一堆 switch 状态):
void Cancel() {
switch (Status) {
case Draft: ...; break;
case Placed: ...; break;
case Shipped: throw "已发货不能取消"; break;
...
}
}
void ChangeAddress() { switch (Status) {...} }
void Pay() { switch (Status) {...} }
病:
✗ 每个方法都重复判断状态,switch 满天飞
✗ 加新状态 = 改所有方法的 switch
✗ 状态流转规则散落,看不出"某状态下能做什么"
反转:把"每种状态下的行为"封装成对象,让当前状态对象决定行为。
→ 这就是状态模式。
|
1
2
3
4
5
6
7
8
9
10
11
| 意图:对象行为随状态变化,把每种状态封装成类,自动切换。
结构:
IState ← 状态接口(Cancel/Pay/ChangeAddress)
ConcreteState ← 每种状态一个实现(DraftState/PaidState...)
Context ← 持有当前状态,把行为委托给它
vs 一堆 switch:
✓ 每种状态的行为内聚在一个类
✓ 加新状态 = 加一个状态类,老的不动(开闭)
✓ 状态流转规则集中、清晰
|
五、状态模式 vs DDD 的状态机
在 DDD 第二篇里,订单聚合的状态流转是这样写的:
1
2
3
4
5
6
7
8
9
10
11
12
13
| public class Order : Entity<OrderId>, IAggregateRoot
{
public OrderStatus Status { get; private set; }
public void Cancel(string reason)
{
if (Status >= OrderStatus.Shipped)
throw new DomainException("已发货不能取消");
Status = OrderStatus.Cancelled;
}
public void MarkPaid() { /* 仅 Placed → Paid */ }
public void Ship() { /* 仅 Paid → Shipped */ }
}
|
1
2
3
4
5
6
7
8
9
10
11
| 这是状态模式的"轻量版":
- 状态用枚举表示(OrderStatus)
- 行为内聚在聚合根方法里(Cancel/MarkPaid/Ship 各自校验 + 流转)
- 没有为每种状态单独建类(订单状态不多,枚举够用)
对比完整状态模式(每种状态一个类):
当状态非常多、每个状态行为复杂、流转规则庞大时,
才值得拆成 IState + 各 State 类。
订单这种 5 个状态、规则简单的场景,枚举 + 聚合方法就够(YAGNI)。
→ DDD 的 OrderStatus 是状态模式的务实落地,不是完整形态。
|
六、状态 vs 策略(极易混)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| 两者结构几乎一样(都是"持有当前对象,委托行为"),但意图不同:
策略模式:客户端【主动选】一个算法
→ 算法之间平等,可随时替换,互不转换
→ 例:选折扣算法、选排序方式
→ "用哪个"由外部决定
状态模式:对象【根据当前状态】自动表现不同行为,状态间会【转换】
→ 状态之间有流转关系(Draft → Placed → Paid)
→ "现在是哪个"由对象自己管理
→ 例:订单状态机、TCP 连接状态
鉴别:
选项会互相转换、有生命周期 → 状态
选项平行、随时换、互不转换 → 策略
|
七、何时用 / 何时别用
1
2
3
4
5
6
7
8
9
10
| 命令模式:
✓ 需要撤销/重做、命令队列、宏录制、任务调度
✓ 调用者与执行者解耦(CQRS、事件驱动)
✗ 简单一次性调用、不需要撤销/排队 → 直接调方法
状态模式:
✓ 对象状态多、每状态行为不同、状态间有流转
✓ switch(state) 满天飞、加状态要改一堆方法
✗ 状态少、规则简单 → 枚举 + 几个 if 就够(DDD 订单就是)
✗ 是"选算法"而非"状态流转" → 那是策略
|
八、小结
- 命令模式:把请求封装成对象 → 可撤销/排队/记录/解耦
- 手写:ICommand(Execute/Undo)+ 接收者 + 历史栈
- .NET 高阶:CQRS 的 Command + MediatR(呼应 DDD 第五篇)
- 状态模式:把每种状态的行为封装成类,行为随状态变
- 解决 switch(state) 满天飞,加状态加类(开闭)
- DDD 的 OrderStatus 是它的轻量落地(枚举够用就别拆类)
- 状态 vs 策略:状态有流转/生命周期,策略是平行可替换
- 务实原则:简单场景用枚举/直接调用,复杂了再上完整模式
九、设计模式系列总结
九篇下来,一条主线:
- (一)SOLID 是底座:把变化关进笼子,所有模式的共同骨架
- (二)策略:算法族可替换,干掉 switch
- (三)工厂:封装创建,DI 容器是终极形态
- (四)建造者:分步构造,对抗伸缩构造函数
- (五)装饰器:透明叠加功能,组合优于继承
- (六)责任链:处理者串链,ASP.NET Core 中间件的真相
- (七)观察者:一对多通知,C# event 天生支持
- (八)单例:全局唯一,优先 DI Singleton 别手写
- (九)命令与状态:请求即对象(CQRS)、行为随状态变(状态机)
回到第一篇的初心:模式是"有名字的设计经验",不是教条。.NET 里很多模式已经融化进语言和框架(event/委托/DI/ LINQ),你不必死记 UML,要记的是意图——什么问题、用什么解、付出什么代价。
和 DDD 系列、微服务系列合在一起看,你会发现它们都在讲同一件事:把复杂度控制在边界内、把变化封装起来、让代码对扩展开放对修改关闭。SOLID 是原则,模式是套路,DDD 是建模,架构是组合——殊途同归。真正理解了这一层,写出来的代码,天然就是好代码。