设计模式:命令模式

欢迎你来读这篇博客,这篇博客主要是关于命令模式
其中包括命令模式的核心思想、适用场景、优缺点、命令队列、撤销操作、异步执行、与策略模式/责任链模式/事件模式的区别,以及 Java 后端开发中订单操作命令总线的完整案例。

序言

在 Java 后端开发中,我们经常会遇到这样的问题:

一个调用者想触发某个操作,但它不应该直接知道这个操作具体由谁执行、怎么执行、什么时候执行。

比如后台管理系统中有很多订单操作:

  • 创建订单;
  • 取消订单;
  • 支付订单;
  • 发货订单;
  • 关闭订单;
  • 退款订单;
  • 审核订单;
  • 批量导出订单。

最直接的写法是:

1
2
3
4
5
orderService.createOrder(command);
orderService.cancelOrder(orderId);
orderService.payOrder(orderId);
orderService.shipOrder(orderId);
orderService.refundOrder(orderId);

这当然可以。

但如果后面出现这些需求:

  • 操作要排队执行;
  • 操作要异步执行;
  • 操作要记录操作日志;
  • 操作失败后要重试;
  • 操作需要支持撤销;
  • 操作要统一鉴权;
  • 操作要统一落库审计;
  • 操作要支持批量组合;
  • 操作要可以延迟执行;
  • 操作要从前端配置或 MQ 消息中动态触发。

这时如果调用者直接依赖具体 Service 方法,就会越来越难扩展。

命令模式就是为了解决这类问题:

把一个请求封装成一个对象,从而让请求可以被参数化、排队、记录、撤销、重试、异步执行。

简单说:

命令模式就是把“我要做什么”封装成一个对象。

这个对象本身就是一条命令。

调用者不需要知道命令怎么执行,只需要把命令交给执行器。

这就像你在餐厅点菜,不需要冲进后厨告诉厨师“先热锅、再下油、再放菜”,你只需要下单。菜单就是命令,服务员是调用者,厨师是接收者,后厨系统是命令执行器。

当然,如果你点的是“随便”,那不是命令模式,那是产品经理需求不明确。

正文

chapter 1:什么是命令模式

命令模式,英文是 Command Pattern,属于行为型设计模式。

它的定义是:

将一个请求封装为一个对象,从而使你可以用不同的请求对客户端进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。

这句话很长,但核心只有一个:

把请求封装成对象。

在命令模式中,一个操作不再只是一个方法调用,而是一个对象。

例如:

1
2
3
Command command = new CancelOrderCommand(orderId);

command.execute();

这个 CancelOrderCommand 对象就代表“取消订单”这个请求。

它可以被:

  • 立即执行;
  • 放进队列;
  • 保存到数据库;
  • 发送到 MQ;
  • 延迟执行;
  • 批量执行;
  • 失败重试;
  • 记录日志;
  • 执行撤销。

一旦请求变成对象,它就可以像普通对象一样被传递、组合、存储和调度。

这就是命令模式最重要的价值。

chapter 2:命令模式的核心角色

命令模式一般包含五个角色:

  1. Command 抽象命令:定义命令执行接口。
  2. ConcreteCommand 具体命令:封装具体请求,调用接收者完成操作。
  3. Receiver 接收者:真正执行业务逻辑的对象。
  4. Invoker 调用者:负责触发命令执行。
  5. Client 客户端:创建命令对象,并设置接收者。

结构如下:

1
2
3
4
5
6
7
8
9
10
Client


Invoker


Command


ConcreteCommand ─────► Receiver

用订单系统来理解:

命令模式角色 订单系统示例
Command OrderCommand
ConcreteCommand CreateOrderCommand、CancelOrderCommand
Receiver OrderService、PaymentService、InventoryService
Invoker CommandInvoker、CommandBus
Client Controller、MQ Listener、定时任务

命令对象并不一定自己完成所有业务逻辑。

它通常会调用 Receiver。

例如:

1
2
3
4
5
6
7
8
9
10
11
public class CancelOrderCommand implements OrderCommand {

private final OrderService orderService;

private final Long orderId;

@Override
public void execute() {
orderService.cancelOrder(orderId);
}
}

真正取消订单的是 OrderService

命令对象负责封装这次请求。

chapter 3:为什么需要命令模式

命令模式解决的核心问题是:

调用者和执行者之间耦合太强。

如果 Controller 直接调用具体 Service:

1
orderService.cancelOrder(orderId);

Controller 就必须知道:

  • 调哪个 Service;
  • 调哪个方法;
  • 传哪些参数;
  • 什么时候执行;
  • 失败如何处理;
  • 是否要重试;
  • 是否要记录操作日志。

如果操作越来越多,调用方就会越来越复杂。

命令模式把这些操作封装成对象:

1
2
3
OrderCommand command = new CancelOrderCommand(orderService, orderId);

commandBus.dispatch(command);

调用方只负责创建命令和提交命令。

执行细节交给命令对象和命令总线。

这样可以带来几个好处:

  • 调用者和执行者解耦;
  • 操作可以被统一调度;
  • 操作可以排队;
  • 操作可以异步;
  • 操作可以记录日志;
  • 操作可以撤销;
  • 操作可以组合;
  • 操作可以延迟执行。

chapter 4:命令模式适合哪些问题

命令模式适合的不是普通方法调用,而是“请求需要被对象化”的场景。

例如:

1. 请求需要排队

例如:

1
2
3
4
订单导出任务
批量短信发送任务
报表生成任务
大文件处理任务

命令对象可以放进队列,后续由执行器慢慢消费。

2. 请求需要异步执行

例如:

1
commandExecutor.submit(command);

命令对象可以提交给线程池、MQ、任务调度器。

3. 请求需要记录日志

命令对象可以包含操作人、操作时间、操作参数。

适合审计日志。

4. 请求需要撤销

如果命令对象支持 undo(),就可以实现撤销操作。

例如编辑器撤销、订单操作回滚、配置变更回滚。

5. 请求需要统一增强

例如统一鉴权、幂等、重试、限流、监控。

命令总线可以在执行命令前后统一处理。

6. 请求需要组合

多个命令可以组成一个宏命令。

例如:

1
创建订单 -> 锁定库存 -> 创建支付单 -> 发送通知

chapter 5:最简单的命令模式示例

先看一个遥控器控制电灯的经典例子。

电灯是接收者:

1
2
3
4
5
6
7
8
9
10
public class Light {

public void on() {
System.out.println("打开电灯");
}

public void off() {
System.out.println("关闭电灯");
}
}

命令接口:

1
2
3
4
public interface Command {

void execute();
}

开灯命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LightOnCommand implements Command {

private final Light light;

public LightOnCommand(Light light) {
this.light = light;
}

@Override
public void execute() {
light.on();
}
}

关灯命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LightOffCommand implements Command {

private final Light light;

public LightOffCommand(Light light) {
this.light = light;
}

@Override
public void execute() {
light.off();
}
}

遥控器是调用者:

1
2
3
4
5
6
7
8
9
10
11
12
public class RemoteController {

private Command command;

public void setCommand(Command command) {
this.command = command;
}

public void pressButton() {
command.execute();
}
}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class CommandDemo {

public static void main(String[] args) {
Light light = new Light();

Command lightOnCommand = new LightOnCommand(light);

RemoteController controller = new RemoteController();
controller.setCommand(lightOnCommand);

controller.pressButton();
}
}

这个例子虽然简单,但它体现了命令模式的核心:

遥控器不直接依赖电灯,而是依赖命令接口。

遥控器只知道执行命令。

至于命令背后是开灯、关灯、开空调,遥控器不关心。

chapter 6:案例背景:订单操作命令总线

下面用 Java 后端更真实的案例来讲命令模式。

假设我们正在开发一个订单系统。

订单有多种操作:

  • 创建订单;
  • 取消订单;
  • 支付订单;
  • 发货订单;
  • 关闭订单。

我们希望实现:

  1. 每个操作封装成一个命令对象;
  2. 调用方只提交命令,不直接调用具体 Service;
  3. 命令执行前后可以统一打印日志;
  4. 命令可以放入队列;
  5. 命令可以支持撤销;
  6. Spring Boot 中可以根据命令类型路由到对应处理器。

这个案例会分几步实现:

  • 普通命令模式;
  • 支持撤销;
  • 支持命令队列;
  • Spring Boot 命令总线。

chapter 7:定义订单模型 Order

先定义一个简单订单对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Order {

private final Long orderId;

private String status;

public Order(Long orderId, String status) {
this.orderId = orderId;
this.status = status;
}

public Long getOrderId() {
return orderId;
}

public String getStatus() {
return status;
}

public void changeStatus(String status) {
this.status = status;
}
}

这里为了演示,只保留订单 ID 和订单状态。

真实项目中,订单会复杂得多。

chapter 8:定义接收者 OrderService

接收者是真正执行业务逻辑的对象。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

public class OrderService {

private final AtomicLong idGenerator = new AtomicLong(10000);

private final Map<Long, Order> orderStore = new ConcurrentHashMap<>();

public Long createOrder(Long userId, Long productId, Integer quantity) {
Long orderId = idGenerator.incrementAndGet();

Order order = new Order(orderId, "CREATED");

orderStore.put(orderId, order);

System.out.println("创建订单,orderId = " + orderId
+ ",userId = " + userId
+ ",productId = " + productId
+ ",quantity = " + quantity);

return orderId;
}

public void cancelOrder(Long orderId) {
Order order = getOrder(orderId);

order.changeStatus("CANCELLED");

System.out.println("取消订单,orderId = " + orderId);
}

public void payOrder(Long orderId) {
Order order = getOrder(orderId);

order.changeStatus("PAID");

System.out.println("支付订单,orderId = " + orderId);
}

public void shipOrder(Long orderId) {
Order order = getOrder(orderId);

order.changeStatus("SHIPPED");

System.out.println("发货订单,orderId = " + orderId);
}

public void changeStatus(Long orderId, String status) {
Order order = getOrder(orderId);

order.changeStatus(status);

System.out.println("修改订单状态,orderId = " + orderId + ",status = " + status);
}

public Order getOrder(Long orderId) {
Order order = orderStore.get(orderId);

if (order == null) {
throw new IllegalArgumentException("order not found, orderId = " + orderId);
}

return order;
}
}

OrderService 是命令模式中的 Receiver。

它不关心命令对象,也不关心谁调用它。

它只负责订单业务能力。

chapter 9:定义命令接口 OrderCommand

1
2
3
4
5
6
public interface OrderCommand {

String name();

void execute();
}

每个命令都需要有:

  • 命令名称;
  • 执行方法。

命令名称可以用于日志、监控、审计。

chapter 10:定义创建订单命令

创建订单命令封装了创建订单所需的参数。

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
27
28
29
30
31
32
33
34
35
36
public class CreateOrderCommand implements OrderCommand {

private final OrderService orderService;

private final Long userId;

private final Long productId;

private final Integer quantity;

private Long createdOrderId;

public CreateOrderCommand(OrderService orderService,
Long userId,
Long productId,
Integer quantity) {
this.orderService = orderService;
this.userId = userId;
this.productId = productId;
this.quantity = quantity;
}

@Override
public String name() {
return "CreateOrderCommand";
}

@Override
public void execute() {
this.createdOrderId = orderService.createOrder(userId, productId, quantity);
}

public Long getCreatedOrderId() {
return createdOrderId;
}
}

这个命令对象中保存了:

  • 接收者 OrderService
  • 用户 ID;
  • 商品 ID;
  • 数量;
  • 创建后的订单 ID。

调用者可以通过命令对象拿到执行结果。

chapter 11:定义取消订单命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CancelOrderCommand implements OrderCommand {

private final OrderService orderService;

private final Long orderId;

public CancelOrderCommand(OrderService orderService, Long orderId) {
this.orderService = orderService;
this.orderId = orderId;
}

@Override
public String name() {
return "CancelOrderCommand";
}

@Override
public void execute() {
orderService.cancelOrder(orderId);
}
}

取消订单命令只关心取消订单所需信息。

chapter 12:定义支付订单命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class PayOrderCommand implements OrderCommand {

private final OrderService orderService;

private final Long orderId;

public PayOrderCommand(OrderService orderService, Long orderId) {
this.orderService = orderService;
this.orderId = orderId;
}

@Override
public String name() {
return "PayOrderCommand";
}

@Override
public void execute() {
orderService.payOrder(orderId);
}
}

chapter 13:定义发货订单命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ShipOrderCommand implements OrderCommand {

private final OrderService orderService;

private final Long orderId;

public ShipOrderCommand(OrderService orderService, Long orderId) {
this.orderService = orderService;
this.orderId = orderId;
}

@Override
public String name() {
return "ShipOrderCommand";
}

@Override
public void execute() {
orderService.shipOrder(orderId);
}
}

chapter 14:定义调用者 CommandInvoker

调用者负责触发命令执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CommandInvoker {

public void invoke(OrderCommand command) {
if (command == null) {
throw new IllegalArgumentException("command can not be null");
}

long start = System.currentTimeMillis();

System.out.println("[Invoker] 开始执行命令:" + command.name());

try {
command.execute();

System.out.println("[Invoker] 命令执行成功:" + command.name());
} finally {
long cost = System.currentTimeMillis() - start;

System.out.println("[Invoker] 命令耗时:" + cost + " ms");
}
}
}

这里就可以统一加:

  • 日志;
  • 耗时统计;
  • 异常处理;
  • 监控埋点;
  • 审计记录。

命令对象只关心业务请求。

调用者负责命令执行过程管理。

chapter 15:客户端使用命令模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CommandPatternDemo {

public static void main(String[] args) {
OrderService orderService = new OrderService();

CommandInvoker invoker = new CommandInvoker();

CreateOrderCommand createCommand = new CreateOrderCommand(
orderService,
1L,
100L,
2
);

invoker.invoke(createCommand);

Long orderId = createCommand.getCreatedOrderId();

invoker.invoke(new PayOrderCommand(orderService, orderId));

invoker.invoke(new ShipOrderCommand(orderService, orderId));
}
}

输出类似:

1
2
3
4
5
6
7
8
9
10
11
12
[Invoker] 开始执行命令:CreateOrderCommand
创建订单,orderId = 10001,userId = 1,productId = 100,quantity = 2
[Invoker] 命令执行成功:CreateOrderCommand
[Invoker] 命令耗时:2 ms
[Invoker] 开始执行命令:PayOrderCommand
支付订单,orderId = 10001
[Invoker] 命令执行成功:PayOrderCommand
[Invoker] 命令耗时:1 ms
[Invoker] 开始执行命令:ShipOrderCommand
发货订单,orderId = 10001
[Invoker] 命令执行成功:ShipOrderCommand
[Invoker] 命令耗时:1 ms

可以看到,客户端没有直接调用:

1
orderService.payOrder(orderId);

而是创建命令并交给 Invoker 执行。

这就是命令模式的基本形式。

chapter 16:支持撤销的命令

命令模式有一个很经典的能力:撤销。

要支持撤销,可以定义一个新的接口。

1
2
3
4
public interface UndoableCommand extends OrderCommand {

void undo();
}

撤销的关键是:

命令在执行前要保存足够的旧状态,撤销时才能恢复。

比如取消订单命令。

取消前订单可能是 CREATED

执行取消后变成 CANCELLED

撤销时要恢复成原来的状态。

chapter 17:可撤销取消订单命令

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
27
28
29
30
31
32
33
34
35
36
37
38
public class UndoableCancelOrderCommand implements UndoableCommand {

private final OrderService orderService;

private final Long orderId;

private String oldStatus;

public UndoableCancelOrderCommand(OrderService orderService, Long orderId) {
this.orderService = orderService;
this.orderId = orderId;
}

@Override
public String name() {
return "UndoableCancelOrderCommand";
}

@Override
public void execute() {
Order order = orderService.getOrder(orderId);

this.oldStatus = order.getStatus();

orderService.cancelOrder(orderId);
}

@Override
public void undo() {
if (oldStatus == null) {
throw new IllegalStateException("command has not been executed");
}

orderService.changeStatus(orderId, oldStatus);

System.out.println("撤销取消订单,orderId = " + orderId + ",恢复状态:" + oldStatus);
}
}

这个命令在执行前保存旧状态:

1
this.oldStatus = order.getStatus();

撤销时恢复:

1
orderService.changeStatus(orderId, oldStatus);

chapter 18:支持撤销的 Invoker

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
27
28
29
30
31
32
import java.util.ArrayDeque;
import java.util.Deque;

public class UndoableCommandInvoker {

private final Deque<UndoableCommand> history = new ArrayDeque<>();

public void invoke(UndoableCommand command) {
if (command == null) {
throw new IllegalArgumentException("command can not be null");
}

System.out.println("[UndoableInvoker] 执行命令:" + command.name());

command.execute();

history.push(command);
}

public void undoLast() {
if (history.isEmpty()) {
System.out.println("[UndoableInvoker] 没有可撤销命令");
return;
}

UndoableCommand command = history.pop();

System.out.println("[UndoableInvoker] 撤销命令:" + command.name());

command.undo();
}
}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class UndoCommandDemo {

public static void main(String[] args) {
OrderService orderService = new OrderService();

CreateOrderCommand createCommand = new CreateOrderCommand(orderService, 1L, 100L, 2);
createCommand.execute();

Long orderId = createCommand.getCreatedOrderId();

UndoableCommandInvoker invoker = new UndoableCommandInvoker();

UndoableCommand cancelCommand = new UndoableCancelOrderCommand(orderService, orderId);

invoker.invoke(cancelCommand);

invoker.undoLast();
}
}

撤销功能非常适合:

  • 文本编辑器;
  • 图形编辑器;
  • 配置变更;
  • 工作流操作;
  • 后台管理操作;
  • 游戏操作;
  • 一些可补偿业务动作。

但是要注意:

不是所有业务都能简单撤销。

比如支付成功、短信已发送、库存已真实扣减,这些动作可能不能简单 undo()

真实业务里更多使用“补偿命令”,而不是简单回滚。

chapter 19:命令队列

命令模式的另一个常见能力是队列化。

因为命令是对象,所以可以放进队列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.LinkedList;
import java.util.Queue;

public class CommandQueue {

private final Queue<OrderCommand> queue = new LinkedList<>();

public void add(OrderCommand command) {
queue.offer(command);
}

public void executeAll() {
while (!queue.isEmpty()) {
OrderCommand command = queue.poll();

System.out.println("[CommandQueue] 执行命令:" + command.name());

command.execute();
}
}
}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CommandQueueDemo {

public static void main(String[] args) {
OrderService orderService = new OrderService();

CommandQueue queue = new CommandQueue();

CreateOrderCommand createCommand1 = new CreateOrderCommand(orderService, 1L, 100L, 2);
CreateOrderCommand createCommand2 = new CreateOrderCommand(orderService, 2L, 200L, 1);

queue.add(createCommand1);
queue.add(createCommand2);

queue.executeAll();
}
}

命令队列适合:

  • 异步任务;
  • 批量操作;
  • 延迟执行;
  • 削峰填谷;
  • 后台任务调度。

如果队列换成 MQ,本质也是类似思想:

把请求封装成消息,异步交给消费者执行。

chapter 20:异步命令执行

命令也可以提交到线程池异步执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class AsyncCommandInvoker {

private final ExecutorService executorService = Executors.newFixedThreadPool(4);

public void submit(OrderCommand command) {
executorService.submit(() -> {
System.out.println("[AsyncInvoker] 异步执行命令:" + command.name());

command.execute();
});
}

public void shutdown() {
executorService.shutdown();
}
}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class AsyncCommandDemo {

public static void main(String[] args) {
OrderService orderService = new OrderService();

AsyncCommandInvoker invoker = new AsyncCommandInvoker();

invoker.submit(new CreateOrderCommand(orderService, 1L, 100L, 2));
invoker.submit(new CreateOrderCommand(orderService, 2L, 200L, 1));

invoker.shutdown();
}
}

真实项目中,异步命令可以结合:

  • 线程池;
  • MQ;
  • 延迟队列;
  • 定时任务;
  • Disruptor;
  • Job 调度框架;
  • 任务表。

不过要注意:

一旦异步执行,就要考虑幂等、重试、失败补偿、上下文传递、TraceId、事务边界。

异步不是魔法,是把复杂度从当前线程搬到了另一个地方。搬家不是消失,箱子还得拆。

chapter 21:宏命令

宏命令就是把多个命令组合成一个命令。

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
import java.util.List;

public class MacroOrderCommand implements OrderCommand {

private final String name;

private final List<OrderCommand> commands;

public MacroOrderCommand(String name, List<OrderCommand> commands) {
this.name = name;
this.commands = List.copyOf(commands);
}

@Override
public String name() {
return name;
}

@Override
public void execute() {
for (OrderCommand command : commands) {
command.execute();
}
}
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MacroCommandDemo {

public static void main(String[] args) {
OrderService orderService = new OrderService();

CreateOrderCommand createCommand = new CreateOrderCommand(orderService, 1L, 100L, 2);
createCommand.execute();

Long orderId = createCommand.getCreatedOrderId();

MacroOrderCommand macroCommand = new MacroOrderCommand(
"PayAndShipMacroCommand",
List.of(
new PayOrderCommand(orderService, orderId),
new ShipOrderCommand(orderService, orderId)
)
);

macroCommand.execute();
}
}

宏命令适合批量操作。

例如:

  • 批量开关设备;
  • 批量修改配置;
  • 批量审批;
  • 批量执行后台任务;
  • 多步骤业务动作。

chapter 22:Spring Boot 中的命令模式落地

在 Spring Boot 项目中,更常见的写法不是让命令对象直接持有 Service,而是:

命令对象只保存参数,命令处理器负责执行业务。

这更接近 CQRS、Command Bus 的风格。

例如:

1
2
3
CreateOrderCommandDTO
CreateOrderCommandHandler
CommandBus

这种方式更适合企业级后端项目。

chapter 23:定义命令标记接口

1
2
public interface Command {
}

命令对象只是数据载体。

例如创建订单命令:

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 class CreateOrderCommand implements Command {

private final Long userId;

private final Long productId;

private final Integer quantity;

public CreateOrderCommand(Long userId, Long productId, Integer quantity) {
this.userId = userId;
this.productId = productId;
this.quantity = quantity;
}

public Long getUserId() {
return userId;
}

public Long getProductId() {
return productId;
}

public Integer getQuantity() {
return quantity;
}
}

取消订单命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class CancelOrderCommand implements Command {

private final Long orderId;

private final String reason;

public CancelOrderCommand(Long orderId, String reason) {
this.orderId = orderId;
this.reason = reason;
}

public Long getOrderId() {
return orderId;
}

public String getReason() {
return reason;
}
}

chapter 24:定义命令处理器接口

1
2
3
4
5
6
public interface CommandHandler<C extends Command, R> {

Class<C> commandType();

R handle(C command);
}

每个处理器声明自己支持的命令类型。

例如:

1
Class<C> commandType();

命令总线可以根据类型找到对应处理器。

chapter 25:创建订单命令处理器

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
import org.springframework.stereotype.Component;

@Component
public class CreateOrderCommandHandler implements CommandHandler<CreateOrderCommand, Long> {

private final OrderApplicationService orderApplicationService;

public CreateOrderCommandHandler(OrderApplicationService orderApplicationService) {
this.orderApplicationService = orderApplicationService;
}

@Override
public Class<CreateOrderCommand> commandType() {
return CreateOrderCommand.class;
}

@Override
public Long handle(CreateOrderCommand command) {
return orderApplicationService.createOrder(
command.getUserId(),
command.getProductId(),
command.getQuantity()
);
}
}

这里的 Receiver 可以是应用服务:

1
OrderApplicationService

命令处理器负责把命令交给应用服务执行。

chapter 26:取消订单命令处理器

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
import org.springframework.stereotype.Component;

@Component
public class CancelOrderCommandHandler implements CommandHandler<CancelOrderCommand, Void> {

private final OrderApplicationService orderApplicationService;

public CancelOrderCommandHandler(OrderApplicationService orderApplicationService) {
this.orderApplicationService = orderApplicationService;
}

@Override
public Class<CancelOrderCommand> commandType() {
return CancelOrderCommand.class;
}

@Override
public Void handle(CancelOrderCommand command) {
orderApplicationService.cancelOrder(
command.getOrderId(),
command.getReason()
);

return null;
}
}

chapter 27:定义命令总线 CommandBus

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Component
public class CommandBus {

private final Map<Class<?>, CommandHandler<?, ?>> handlerMap;

public CommandBus(List<CommandHandler<?, ?>> handlers) {
this.handlerMap = handlers.stream()
.collect(Collectors.toUnmodifiableMap(CommandHandler::commandType, handler -> handler));
}

@SuppressWarnings("unchecked")
public <C extends Command, R> R dispatch(C command) {
if (command == null) {
throw new IllegalArgumentException("command can not be null");
}

CommandHandler<C, R> handler = (CommandHandler<C, R>) handlerMap.get(command.getClass());

if (handler == null) {
throw new IllegalArgumentException("No command handler found for " + command.getClass().getName());
}

long start = System.currentTimeMillis();

System.out.println("[CommandBus] 开始分发命令:" + command.getClass().getSimpleName());

try {
R result = handler.handle(command);

System.out.println("[CommandBus] 命令执行成功:" + command.getClass().getSimpleName());

return result;
} finally {
long cost = System.currentTimeMillis() - start;

System.out.println("[CommandBus] 命令耗时:" + cost + " ms");
}
}
}

这里做了几件事:

  1. 启动时收集所有 CommandHandler
  2. 按命令类型建立映射;
  3. 接收到命令后查找对应 Handler;
  4. 调用 Handler 执行命令;
  5. 统一记录日志和耗时。

这就是后端项目里非常常见的命令总线。

chapter 28:Controller 使用 CommandBus

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
27
28
29
30
31
32
33
34
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/orders")
public class OrderController {

private final CommandBus commandBus;

public OrderController(CommandBus commandBus) {
this.commandBus = commandBus;
}

@PostMapping
public Long createOrder(@RequestBody CreateOrderRequest request) {
CreateOrderCommand command = new CreateOrderCommand(
request.getUserId(),
request.getProductId(),
request.getQuantity()
);

return commandBus.dispatch(command);
}

@PostMapping("/{orderId}/cancel")
public void cancelOrder(@PathVariable Long orderId,
@RequestBody CancelOrderRequest request) {
CancelOrderCommand command = new CancelOrderCommand(
orderId,
request.getReason()
);

commandBus.dispatch(command);
}
}

请求 DTO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CreateOrderRequest {

private Long userId;

private Long productId;

private Integer quantity;

public Long getUserId() {
return userId;
}

public Long getProductId() {
return productId;
}

public Integer getQuantity() {
return quantity;
}
}
1
2
3
4
5
6
7
8
public class CancelOrderRequest {

private String reason;

public String getReason() {
return reason;
}
}

这种写法的好处是:

  • Controller 很薄;
  • 命令对象表达业务意图;
  • 命令处理器职责清晰;
  • 新增命令只需要新增 Command + Handler;
  • CommandBus 可以统一做横切增强。

chapter 29:CommandBus 可以做哪些统一增强

命令总线是一个很好的统一入口。

它可以做很多事情:

1. 日志

1
2
3
4
执行了什么命令
谁执行的
执行参数是什么
执行结果是什么

2. 审计

例如后台操作审计:

1
管理员 A 在 2026-04-01 取消了订单 10001

3. 权限校验

可以根据命令类型检查权限:

1
2
CreateOrderCommand -> order:create
CancelOrderCommand -> order:cancel

4. 幂等

可以通过命令中的 requestId 做幂等控制。

5. 事务

可以在 Handler 或应用服务层控制事务。

6. 监控

可以统计每类命令的执行次数、耗时、失败率。

7. 异步分发

某些命令可以进入异步队列。

8. 重试

对可重试命令做统一失败重试。

不过要注意:

CommandBus 可以做横切增强,但不要把业务规则全部塞进去。

CommandBus 是调度中心,不是业务垃圾桶。

chapter 30:命令模式和 CQRS

命令模式和 CQRS 经常一起出现。

CQRS 是 Command Query Responsibility Segregation,命令查询职责分离。

它强调:

  • Command 负责改变系统状态;
  • Query 负责查询系统状态。

例如:

1
2
3
CreateOrderCommand
CancelOrderCommand
PayOrderCommand

这些是命令,会修改状态。

1
2
GetOrderDetailQuery
ListUserOrdersQuery

这些是查询,不应该修改状态。

命令模式可以作为 CQRS 中 Command 部分的实现方式。

例如:

1
2
commandBus.dispatch(new CreateOrderCommand(...));
queryBus.query(new GetOrderDetailQuery(...));

这不是必须的,但在复杂业务系统中很常见。

chapter 31:命令模式和事件模式的区别

命令模式和事件模式很容易混。

它们都可以是一个对象,都可以放进 MQ。

但语义完全不同。

对比项 命令 Command 事件 Event
语义 希望系统去做某件事 表示某件事已经发生
命名 CreateOrderCommand OrderCreatedEvent
时间方向 将来要执行 过去已经发生
接收者数量 通常一个明确处理者 可以有多个订阅者
是否期望结果 通常期望结果 通常不直接期望结果
示例 取消订单 订单已取消

命令是:

1
请你取消这个订单。

事件是:

1
这个订单已经取消了。

这两个语义千万不要混。

如果用户点击“取消订单”,发送的是命令:

1
CancelOrderCommand

订单取消成功后发布的是事件:

1
OrderCancelledEvent

命令是意图。

事件是事实。

chapter 32:命令模式和策略模式的区别

命令模式和策略模式都可以封装行为。

但关注点不同。

对比项 命令模式 策略模式
核心目的 将请求封装成对象 封装可替换算法
是否保存请求参数 通常会保存 通常不保存具体请求
是否支持排队/撤销/日志 适合 不是重点
调用方式 command.execute() strategy.calculate(context)
示例 CancelOrderCommand DiscountStrategy

策略模式解决的是:

多种算法如何替换。

命令模式解决的是:

一个请求如何对象化、调度、记录、撤销。

例如优惠计算:

1
discountStrategy.calculate(order);

这是策略模式。

取消订单:

1
commandBus.dispatch(new CancelOrderCommand(orderId));

这是命令模式。

chapter 33:命令模式和责任链模式的区别

命令模式和责任链模式也常常一起出现。

对比项 命令模式 责任链模式
核心目的 把请求封装成对象 让请求沿处理链传递
关注点 请求对象化 多个处理器协作
是否多个处理器 不一定 通常多个
典型场景 命令总线、任务队列、撤销 校验链、过滤器链、审批链
示例 CreateOrderCommand OrderValidateHandlerChain

它们可以组合使用。

例如:

1
2
3
4
5
6
7
CreateOrderCommand
-> CommandBus
-> CreateOrderCommandHandler
-> OrderCreateValidateChain
-> ParamValidateHandler
-> StockValidateHandler
-> RiskValidateHandler

命令模式负责表达“创建订单”这个请求。

责任链模式负责处理“创建订单前的校验流程”。

chapter 34:命令模式和模板方法模式的区别

对比项 命令模式 模板方法模式
核心目的 请求对象化 固定算法骨架
扩展方式 新增命令类 子类重写步骤
是否适合排队 适合 不适合
是否适合撤销 适合 不是重点
示例 ExportReportCommand AbstractImportTemplate

模板方法适合固定流程。

命令模式适合把操作封装成独立对象。

chapter 35:命令模式的优点

1. 解耦调用者和执行者

调用者只依赖命令接口或命令总线,不直接依赖具体执行逻辑。

2. 请求可以对象化

一旦请求变成对象,就可以传递、保存、排队、重试、撤销。

3. 易于扩展新命令

新增命令时,新增 Command 和 Handler 即可。

4. 支持日志和审计

命令对象天然适合记录操作信息。

5. 支持撤销和重做

命令可以保存旧状态,实现 undo()

6. 支持队列和异步

命令可以进入队列,由后台线程或 MQ 消费。

7. 支持宏命令

多个命令可以组合成一个大命令。

chapter 36:命令模式的缺点

1. 类数量增加

每个操作都可能对应一个命令类和处理器类。

2. 简单场景会显得啰嗦

如果只是一个普通方法调用,命令模式可能过度设计。

3. 命令对象设计需要谨慎

命令里放什么参数、是否可序列化、是否可重试,都需要设计。

4. 撤销并不总是容易

真实业务中的撤销往往不是简单恢复旧状态,而是需要补偿流程。

5. 异步命令会引入更多复杂度

包括幂等、重试、事务、消息丢失、顺序一致性等。

chapter 37:适用场景

命令模式适合以下场景。

1. 请求需要被参数化

例如按钮绑定不同命令。

1
button.setCommand(new SaveCommand());

2. 请求需要排队或异步执行

例如任务队列、后台任务、MQ 消息。

3. 请求需要日志和审计

例如后台管理操作日志。

4. 请求需要撤销和重做

例如编辑器、配置变更、工作流操作。

5. 调用者和执行者需要解耦

例如 Controller 不直接依赖复杂业务执行逻辑。

6. 需要统一命令分发

例如 CommandBus。

7. 需要把操作保存下来之后再执行

例如延迟任务、定时任务、失败重试任务。

chapter 38:不适合使用的场景

以下场景不建议使用命令模式。

1. 普通 CRUD 过于简单

如果只是:

1
userService.getById(id);

没必要包装成:

1
GetUserByIdCommand

不然项目会很快变成类文件批发市场。

2. 操作不需要排队、撤销、异步、日志

如果没有这些需求,直接方法调用更简单。

3. 团队不熟悉命令总线

命令总线会引入间接调用,新人可能需要理解成本。

4. 命令数量失控

如果所有操作都命令化,命令类会非常多。

需要分包、命名、规范管理。

5. 命令和事件语义混乱

不要用 OrderCreatedEvent 去表示“创建订单请求”。

也不要用 CreateOrderCommand 表示“订单已创建事实”。

语义错了,架构后面会非常别扭。

chapter 39:真实项目中的实践建议

1. 命令命名要表达动作意图

推荐:

1
2
3
4
5
CreateOrderCommand
CancelOrderCommand
PayOrderCommand
RefundOrderCommand
ExportOrderCommand

不推荐:

1
2
3
4
OrderCommand
OrderRequest
OrderAction
DoOrderCommand

名字越泛,职责越危险。

2. 命令对象尽量只保存请求参数

不要把复杂业务逻辑塞进命令 DTO。

在 Spring Boot 项目中,更推荐:

1
Command + CommandHandler

而不是命令对象自己持有一堆 Service。

3. 命令处理器要有明确边界

一个 Handler 处理一个命令。

不要一个 Handler 处理几十种命令。

4. CommandBus 只做调度和横切增强

适合做:

  • 日志;
  • 监控;
  • 权限;
  • 幂等;
  • 事务入口;
  • Handler 路由。

不适合做:

  • 订单核心业务;
  • 价格计算;
  • 库存规则;
  • 支付状态机。

5. 异步命令必须考虑幂等

只要进入 MQ 或任务队列,就必须假设命令可能重复执行。

因此要设计:

  • requestId;
  • 业务唯一键;
  • 去重表;
  • 幂等状态机;
  • 分布式锁;
  • 唯一索引。

6. 撤销操作优先考虑补偿命令

真实业务中更推荐:

1
2
3
CancelPaymentCommand
ReleaseStockCommand
RollbackCouponCommand

而不是简单 undo()

因为很多业务动作不可逆,只能补偿。

7. 命令入库要考虑序列化

如果命令要持久化到任务表,需要考虑:

  • 命令类型;
  • 命令参数 JSON;
  • 状态;
  • 重试次数;
  • 下一次执行时间;
  • 错误信息;
  • 创建时间;
  • 更新时间。

8. 命令和事件分清楚

命令是:

1
请做某事

事件是:

1
某事已发生

这个边界非常重要。

9. 简单项目不要过度命令化

命令模式很强,但不要把所有方法都包成命令。

设计模式是工具,不是 KPI。

chapter 40:完整案例代码汇总

订单对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Order {

private final Long orderId;

private String status;

public Order(Long orderId, String status) {
this.orderId = orderId;
this.status = status;
}

public Long getOrderId() {
return orderId;
}

public String getStatus() {
return status;
}

public void changeStatus(String status) {
this.status = status;
}
}

接收者

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

public class OrderService {

private final AtomicLong idGenerator = new AtomicLong(10000);

private final Map<Long, Order> orderStore = new ConcurrentHashMap<>();

public Long createOrder(Long userId, Long productId, Integer quantity) {
Long orderId = idGenerator.incrementAndGet();

Order order = new Order(orderId, "CREATED");

orderStore.put(orderId, order);

System.out.println("创建订单,orderId = " + orderId
+ ",userId = " + userId
+ ",productId = " + productId
+ ",quantity = " + quantity);

return orderId;
}

public void cancelOrder(Long orderId) {
Order order = getOrder(orderId);

order.changeStatus("CANCELLED");

System.out.println("取消订单,orderId = " + orderId);
}

public void payOrder(Long orderId) {
Order order = getOrder(orderId);

order.changeStatus("PAID");

System.out.println("支付订单,orderId = " + orderId);
}

public void shipOrder(Long orderId) {
Order order = getOrder(orderId);

order.changeStatus("SHIPPED");

System.out.println("发货订单,orderId = " + orderId);
}

public void changeStatus(Long orderId, String status) {
Order order = getOrder(orderId);

order.changeStatus(status);

System.out.println("修改订单状态,orderId = " + orderId + ",status = " + status);
}

public Order getOrder(Long orderId) {
Order order = orderStore.get(orderId);

if (order == null) {
throw new IllegalArgumentException("order not found, orderId = " + orderId);
}

return order;
}
}

命令接口

1
2
3
4
5
6
public interface OrderCommand {

String name();

void execute();
}

创建订单命令

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
27
28
29
30
31
32
33
34
35
36
public class CreateOrderCommand implements OrderCommand {

private final OrderService orderService;

private final Long userId;

private final Long productId;

private final Integer quantity;

private Long createdOrderId;

public CreateOrderCommand(OrderService orderService,
Long userId,
Long productId,
Integer quantity) {
this.orderService = orderService;
this.userId = userId;
this.productId = productId;
this.quantity = quantity;
}

@Override
public String name() {
return "CreateOrderCommand";
}

@Override
public void execute() {
this.createdOrderId = orderService.createOrder(userId, productId, quantity);
}

public Long getCreatedOrderId() {
return createdOrderId;
}
}

支付订单命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class PayOrderCommand implements OrderCommand {

private final OrderService orderService;

private final Long orderId;

public PayOrderCommand(OrderService orderService, Long orderId) {
this.orderService = orderService;
this.orderId = orderId;
}

@Override
public String name() {
return "PayOrderCommand";
}

@Override
public void execute() {
orderService.payOrder(orderId);
}
}

发货订单命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ShipOrderCommand implements OrderCommand {

private final OrderService orderService;

private final Long orderId;

public ShipOrderCommand(OrderService orderService, Long orderId) {
this.orderService = orderService;
this.orderId = orderId;
}

@Override
public String name() {
return "ShipOrderCommand";
}

@Override
public void execute() {
orderService.shipOrder(orderId);
}
}

调用者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CommandInvoker {

public void invoke(OrderCommand command) {
if (command == null) {
throw new IllegalArgumentException("command can not be null");
}

long start = System.currentTimeMillis();

System.out.println("[Invoker] 开始执行命令:" + command.name());

try {
command.execute();

System.out.println("[Invoker] 命令执行成功:" + command.name());
} finally {
long cost = System.currentTimeMillis() - start;

System.out.println("[Invoker] 命令耗时:" + cost + " ms");
}
}
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CommandPatternDemo {

public static void main(String[] args) {
OrderService orderService = new OrderService();

CommandInvoker invoker = new CommandInvoker();

CreateOrderCommand createCommand = new CreateOrderCommand(
orderService,
1L,
100L,
2
);

invoker.invoke(createCommand);

Long orderId = createCommand.getCreatedOrderId();

invoker.invoke(new PayOrderCommand(orderService, orderId));

invoker.invoke(new ShipOrderCommand(orderService, orderId));
}
}

chapter 41:一句话总结

命令模式的本质是:

把请求封装成对象,让请求可以被传递、存储、排队、记录、撤销、重试和异步执行。

它特别适合:

  • 操作需要排队;
  • 操作需要异步;
  • 操作需要审计;
  • 操作需要撤销;
  • 操作需要重试;
  • 操作需要统一调度;
  • 调用者和执行者需要解耦;
  • 后端系统需要 CommandBus。

命令模式不是为了把每个方法都包装成类。

它真正适合的是那些“操作本身有生命周期”的场景。

一个普通方法调用,直接调用就好。

一个需要被记录、调度、重试、撤销、异步执行的请求,就值得被封装成命令。

好的命令模式像一张清晰的工单:谁发起、做什么、谁处理、结果如何,都有记录。

坏的命令模式像一堆小纸条:类很多,语义乱,最后没人知道哪张纸条该交给谁。

参考资料

  • Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software.
  • Robert C. Martin. Agile Software Development, Principles, Patterns, and Practices.
  • Martin Fowler. Patterns of Enterprise Application Architecture.
  • Eric Evans. Domain-Driven Design: Tackling Complexity in the Heart of Software.
  • Spring Framework Documentation: Core Technologies - The IoC Container.
  • Spring Framework Documentation: Task Execution and Scheduling.
  • Refactoring Guru: Command Pattern.
  • SourceMaking: Command Design Pattern.

启示录

富贵岂由人,时会高志须酬。

能成功于千载者,必以近察远。


设计模式:命令模式
https://allendericdalexander.github.io/2026/04/01/java/design/15command-pattern-blog/
作者
AtLuoFu
发布于
2026年4月1日
许可协议