一句话定位:将一个请求封装为一个对象,从而可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
一、实战场景
用户下单成功后,系统要做一连串"后置动作":
- 发送订单确认短信/邮件;
- 发放积分;
- 更新会员等级;
- 通知仓储系统备货;
- 记录运营埋点。
如果直接在主流程里逐个调用:
csharp
public async Task OnOrderCreatedAsync(Order order)
{
await _smsService.SendAsync(order.CustomerPhone, "下单成功......");
await _pointsService.GrantAsync(order.CustomerId, 100);
await _memberService.UpgradeAsync(order.CustomerId);
await _warehouseService.NotifyAsync(order);
await _trackingService.TrackAsync("order.created", order);
// 每加一个动作,这里就多一行;失败处理也各不相同......
}
问题:
- 主流程被业务动作塞满,订单核心逻辑和后置动作耦合在一起;
- 这些动作往往需要异步执行、失败重试、排队削峰,直接内联调用做不到;
- 想"批量重放"(比如系统恢复后重跑某天的后置动作)无从下手;
- 每个动作的失败处理策略不同(短信失败可忽略、积分发放失败要重试),散落在主流程里。
命令模式的解法:把每个动作封装成一个"命令对象"(实现统一接口),主流程只负责"创建命令并提交"。命令可以被排队、被记录、被重放、被批量执行------动作本身变成了数据。
二、设计思路
调用方(下单服务)
│ 创建并提交
▼
┌─────────────────────────────┐
│ CommandQueue(命令队列) │
│ 排队 → 逐个执行 → 失败重试 │
└──────────────┬──────────────┘
▼ 执行(Invoke)
┌─────────────────────────────┐
│ IOrderCommand(命令接口) │
│ ├─ SendSmsCommand │
│ ├─ GrantPointsCommand │
│ ├─ UpgradeMemberCommand │
│ ├─ NotifyWarehouseCommand │
│ └─ TrackEventCommand │
└─────────────────────────────┘
命令模式的精髓:"做什么"被封装成对象,可以存储、传输、排队、重放。
三、代码实现(.NET 10 / C# 14)
3.1 命令接口与执行上下文
csharp
namespace Orders.AfterOrder;
public interface IOrderCommand
{
string Name { get; }
Task ExecuteAsync(OrderContext context, CancellationToken ct = default);
Task UndoAsync(OrderContext context, CancellationToken ct = default); // 可选的补偿
}
public sealed record OrderContext(OrderSnapshot Order);
public sealed record OrderSnapshot(string OrderId, string CustomerId, string CustomerPhone, decimal Amount);
3.2 具体命令们(每个动作一个命令)
csharp
namespace Orders.AfterOrder.Commands;
// 命令1:发短信
public sealed class SendSmsCommand(ISmsSender sms) : IOrderCommand
{
public string Name => "SendSms";
public Task ExecuteAsync(OrderContext ctx, CancellationToken ct = default)
=> sms.SendAsync(ctx.Order.CustomerPhone, $"您已成功下单 {ctx.Order.OrderId},金额 {ctx.Order.Amount:C}", ct);
public Task UndoAsync(OrderContext ctx, CancellationToken ct = default) => Task.CompletedTask; // 短信无法撤回
}
// 命令2:发积分(失败可重试)
public sealed class GrantPointsCommand(IPointsService points) : IOrderCommand
{
public string Name => "GrantPoints";
public Task ExecuteAsync(OrderContext ctx, CancellationToken ct = default)
=> points.GrantAsync(ctx.Order.CustomerId, (int)(ctx.Order.Amount / 10), ct);
public Task UndoAsync(OrderContext ctx, CancellationToken ct = default)
=> points.RecoverAsync(ctx.Order.CustomerId, (int)(ctx.Order.Amount / 10), ct);
}
// 命令3:通知仓储
public sealed class NotifyWarehouseCommand(IWarehouseService warehouse) : IOrderCommand
{
public string Name => "NotifyWarehouse";
public Task ExecuteAsync(OrderContext ctx, CancellationToken ct = default)
=> warehouse.NotifyAsync(ctx.Order.OrderId, ct);
public Task UndoAsync(OrderContext ctx, CancellationToken ct = default) => Task.CompletedTask;
}
// 命令4:运营埋点(可忽略失败)
public sealed class TrackEventCommand(ITrackingService tracking) : IOrderCommand
{
public string Name => "TrackEvent";
public Task ExecuteAsync(OrderContext ctx, CancellationToken ct = default)
=> tracking.TrackAsync("order.created", ctx.Order, ct);
public Task UndoAsync(OrderContext ctx, CancellationToken ct = default) => Task.CompletedTask;
}
3.3 执行器:命令队列(排队、重试、记录)
csharp
namespace Orders.AfterOrder;
public sealed class CommandQueue(ILogger logger)
{
private readonly Queue<IOrderCommand> _commands = new();
public void Enqueue(IOrderCommand command) => _commands.Enqueue(command);
public async Task<IReadOnlyList<CommandExecutionResult>> FlushAsync(CancellationToken ct = default)
{
var results = new List<CommandExecutionResult>();
while (_commands.Count > 0)
{
var command = _commands.Dequeue();
var sw = Stopwatch.StartNew();
try
{
await ExecuteWithRetryAsync(command, ct);
results.Add(new CommandExecutionResult(command.Name, true, sw.ElapsedMilliseconds, null));
}
catch (Exception ex)
{
logger.LogError(ex, "命令 {Name} 执行失败", command.Name);
results.Add(new CommandExecutionResult(command.Name, false, sw.ElapsedMilliseconds, ex.Message));
}
}
return results;
}
private async Task ExecuteWithRetryAsync(IOrderCommand command, CancellationToken ct, int maxRetries = 3)
{
for (var attempt = 1; ; attempt++)
{
try
{
await command.ExecuteAsync(CommandContext.Current, ct);
return;
}
catch when (attempt < maxRetries)
{
await Task.Delay(TimeSpan.FromSeconds(attempt), ct); // 指数退避的简化版
}
}
}
}
public sealed record CommandExecutionResult(string Name, bool Success, long ElapsedMs, string? Error);
// 简单模拟全局执行上下文(真实项目可用 AsyncLocal / DI Scope)
public static class CommandContext
{
public static OrderContext Current { get; set; } = default!;
}
3.4 主流程:下单后把动作"打包提交"
csharp
namespace Orders;
public sealed class OrderService(
CommandQueue queue,
SendSmsCommand smsCommand,
GrantPointsCommand pointsCommand,
NotifyWarehouseCommand warehouseCommand,
TrackEventCommand trackCommand)
{
public async Task<OrderSnapshot> CreateOrderAsync(CreateOrderRequest req, CancellationToken ct = default)
{
// 1. 核心下单逻辑(校验、扣库存、建单......省略)
var order = new OrderSnapshot(
OrderId: $"ORD-{Guid.NewGuid():N}"[..18],
CustomerId: req.CustomerId,
CustomerPhone: req.Phone,
Amount: req.Amount);
// 2. 后置动作全部"打包"成命令,进队列
CommandContext.Current = new OrderContext(order);
queue.Enqueue(smsCommand);
queue.Enqueue(pointsCommand);
queue.Enqueue(warehouseCommand);
queue.Enqueue(trackCommand);
// 3. 统一执行(真实项目:队列可换成 Hangfire / RabbitMQ,实现异步削峰)
var results = await queue.FlushAsync(ct);
return order with { /* 可附加 results 状态 */ };
}
}
3.5 组装与运行
csharp
// Program.cs(.NET 10 顶层语句)
using Orders;
using Orders.AfterOrder;
using Orders.AfterOrder.Commands;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddLogging();
// 注册各种服务与命令
builder.Services.AddSingleton<ISmsSender, FakeSmsSender>();
builder.Services.AddSingleton<IPointsService, FakePointsService>();
builder.Services.AddSingleton<IWarehouseService, FakeWarehouseService>();
builder.Services.AddSingleton<ITrackingService, FakeTrackingService>();
builder.Services.AddSingleton<SendSmsCommand>();
builder.Services.AddSingleton<GrantPointsCommand>();
builder.Services.AddSingleton<NotifyWarehouseCommand>();
builder.Services.AddSingleton<TrackEventCommand>();
builder.Services.AddSingleton<CommandQueue>();
builder.Services.AddScoped<OrderService>();
var app = builder.Build();
app.MapPost("/orders", async (CreateOrderRequest req, OrderService orders) =>
{
var order = await orders.CreateOrderAsync(req);
return Results.Ok(new { order.OrderId, Status = "created" });
});
app.Run();
// 各 Fake 服务的实现(演示用,真实项目替换为实际客户端)
public interface ISmsSender { Task SendAsync(string phone, string text, CancellationToken ct); }
public sealed class FakeSmsSender : ISmsSender
{
public Task SendAsync(string phone, string text, CancellationToken ct)
{ Console.WriteLine($"[短信] {phone}: {text}"); return Task.CompletedTask; }
}
public interface IPointsService { Task GrantAsync(string customer, int points, CancellationToken ct); Task RecoverAsync(string customer, int points, CancellationToken ct); }
public sealed class FakePointsService : IPointsService
{
public Task GrantAsync(string customer, int points, CancellationToken ct)
{ Console.WriteLine($"[积分] 用户 {customer} +{points}"); return Task.CompletedTask; }
public Task RecoverAsync(string customer, int points, CancellationToken ct) => Task.CompletedTask;
}
public interface IWarehouseService { Task NotifyAsync(string orderId, CancellationToken ct); }
public sealed class FakeWarehouseService : IWarehouseService
{
public Task NotifyAsync(string orderId, CancellationToken ct)
{ Console.WriteLine($"[仓储] 备货通知 {orderId}"); return Task.CompletedTask; }
}
public interface ITrackingService { Task TrackAsync(string evt, object payload, CancellationToken ct); }
public sealed class FakeTrackingService : ITrackingService
{
public Task TrackAsync(string evt, object payload, CancellationToken ct)
{ Console.WriteLine($"[埋点] {evt}"); return Task.CompletedTask; }
}
public sealed record CreateOrderRequest(string CustomerId, string Phone, decimal Amount);
四、应用要点与注意事项
- 命令模式的三大价值 :解耦 (主流程不再知道后置动作的实现)、可排队 (命令是对象,能进队列、异步执行、削峰)、可重放(命令可记录、可批量重跑、可做补偿)。
- 与"消息队列"的关系:命令模式是"代码层面的封装",消息队列是"执行层面的载体"。两者常组合使用------把命令序列化成消息投递到 RabbitMQ/Kafka,消费者反序列化后执行。理解命令模式,就理解了任务系统的核心。
- Undo 是可选能力 :不是所有命令都能撤销(短信发出去撤不回)。
UndoAsync返回空实现即可,但设计命令接口时保留这个钩子,未来做"补偿/回滚"时不用改接口。 - 区分命令 vs 事件:命令是"明确的意图,期望被执行"(发积分);事件是"已经发生的事实,可以触发副作用"(订单已创建)。下单后置动作既可以用命令模式,也可以用观察者模式(事件驱动)实现------命令模式更强调"可排队、可重放",事件驱动更强调"解耦发布订阅"。两者可以并存。
- 每个命令保持单一职责 :一个命令只做一件事,失败重试策略可以在执行器层面统一(如示例的
ExecuteWithRetryAsync),不要让命令内部各自实现重试。
五、小结
命令模式把"动作"从"代码"升级成了"数据":可排队、可记录、可重放、可补偿。下单后置动作、定时任务、批量操作、撤销重做,都是它的经典舞台。配合消息队列、Hangfire 这类执行载体,它几乎构成了现代后端"异步任务体系"的全部雏形。