跳到主要内容

命令模式

定义

命令模式(command pattern):命令模式将请求(命令)封装为一个对象,这样可以使用不同的请求参数化其它对象(将不同请求依赖注入到其它对象),并且能够支持请求(命令)的排队执行、记录日志、撤销等(附加控制)功能

命令模式的核心是将指令信息封装成一个对象,并将此对象作为参数发送给接收方去执行,达到使命令的请求与执行方解耦,双方只通过传递各种命令对象来完成任务。

在实际的开发中,如果用到的编程语言并不支持用函数作为参数来传递,那么就可以借助命令模式将函数封装为对象来使用。

提示

C 语言支持函数指针,我们可以把函数当作变量传递来传递去。但是,在大部分编程语言中,函数没法作为参数传递给其它函数,也没法赋值给变量。借助命令模式,我们可以将函数封装成对象。具体来说就是,设计一个包含这个函数的类,实例化一个对象传来传去,这样就可以实现把函数像对象一样使用。

模式原理

  • Command:抽象的命令类,定义命令的接口。
  • ConcreteCommand:具体命令角色,通常会持有接收者的引用,并调用接收者的功能来完成命令要执行的相应的功能。
  • Receiver(接收者):真正执行命令的对象。
  • Invoker(调用者):持有命令对象的引用,调用命令对象来执行请求。

设计要点

  1. 解耦请求与执行:通过命令对象将请求发送者与请求执行者解耦
  2. 支持队列操作:可以轻松实现命令的排队执行
  3. 支持撤销操作:可以保存命令历史,实现撤销功能
  4. 支持日志记录:可以记录命令的执行过程

应用案例

模拟酒店后厨的出餐流程,来对命令模式进行演示,命令模式的角色与案例中的角色对应关系如下:

  • 服务员:即调用者角色,由它来发起命令。
  • 厨师:接收者,真正执行命令的对象
  • 订单:命令中包含订单。

核心类实现

1. 抽象命令接口(Command)

/**
* 抽象命令接口
*/
public interface Command {
// 统一的执行方法
void execute();
}

2. 具体命令类(OrderCommand)

/**
* 具体命令
*/
public class OrderCommand implements Command {
// 持有接收者对象引用
private Chef receiver;
private Order order;

public OrderCommand(Chef receiver, Order order) {
this.receiver = receiver;
this.order = order;
}

@Override
public void execute() {
System.out.println(order.getDiningTable() + "桌的订单:");
Set<String> keySet = order.getDooMenu().keySet();
for (String key : keySet) {
Integer num = order.getDooMenu().get(key);
receiver.makeFood(num, key);
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(order.getDiningTable() + "桌的菜品已经上齐了");
}
}

3. 接收者类(Chef)

/**
* 厨师类 -> Receiver 接受者角色
*/
public class Chef {
public void makeFood(int num, String foodName) {
System.out.println(num + "份" + foodName);
}
}

4. 调用者类(Waiter)

import java.util.ArrayList;
import java.util.List;

/**
* 服务员 -> Invoker 调用者
*/
public class Waiter {
// 可以持有多个命令对象
private List<Command> commands;

public Waiter() {
commands = new ArrayList<>();
}

public Waiter(List<Command> commands) {
this.commands = commands;
}

public void addCommands(Command command) {
this.commands.add(command);
}

// 发出指令
public void orderUp() {
System.out.println("叮咚!服务员:有新的订单,请师傅开始制作...");
for (Command command : commands) {
command.execute();
}
}
}

5. 订单类(Order)

import java.util.HashMap;
import java.util.Map;

/**
* 订单类
*/
public class Order {
// 餐桌号码
private int diningTable;
// 存储菜名和份数
private Map<String, Integer> dooMenu = new HashMap<>();

public int getDiningTable() {
return diningTable;
}

public void setDiningTable(int diningTable) {
this.diningTable = diningTable;
}

public Map<String, Integer> getDooMenu() {
return dooMenu;
}

public void setDooMenu(Map<String, Integer> dooMenu) {
this.dooMenu = dooMenu;
}
}

6. 客户端测试(Client)

public class Client {
public static void main(String[] args) {
// 创建订单1
Order order1 = new Order();
order1.setDiningTable(10);
order1.getDooMenu().put("蛋炒饭", 1);
order1.getDooMenu().put("海参炒面", 1);

// 创建订单2
Order order2 = new Order();
order2.setDiningTable(11);
order2.getDooMenu().put("回锅肉", 1);
order2.getDooMenu().put("木须肉", 1);

// 创建接收者
Chef chef = new Chef();

// 将订单和接收者封装成命令对象
Command cmd1 = new OrderCommand(chef, order1);
Command cmd2 = new OrderCommand(chef, order2);

// 创建调用者
Waiter waiter = new Waiter();
waiter.addCommands(cmd1);
waiter.addCommands(cmd2);

// 将订单发送给厨师,上菜
waiter.orderUp();
}
}

运行结果示例

叮咚!服务员:有新的订单,请师傅开始制作...
10桌的订单:
1份蛋炒饭
1份海参炒面
10桌的菜品已经上齐了
11桌的订单:
1份回锅肉
1份木须肉
11桌的菜品已经上齐了

案例分析

1. 角色职责

  • Waiter(调用者):负责接收订单并调用命令执行
  • OrderCommand(具体命令):封装订单信息,调用厨师执行
  • Chef(接收者):实际执行烹饪操作
  • Order(订单):包含订单的详细信息

2. 设计优势

  • 解耦:服务员不需要直接与厨师交互,通过命令对象传递
  • 扩展性:可以轻松添加新的命令类型(如取消订单、修改订单等)
  • 队列支持:可以批量处理多个订单
  • 日志记录:可以记录每个订单的处理过程

3. 实际应用价值

  • 餐厅管理系统:订单处理、菜品制作流程
  • 任务调度系统:任务队列管理、执行状态跟踪
  • 游戏开发:玩家操作记录、撤销功能
  • GUI开发:按钮点击事件、菜单操作

总结

优点

  • 降低系统的耦合度:命令模式能将调用操作的对象与实现该操作的对象解耦。
  • 增加或删除命令非常方便:采用命令模式增加与删除命令不会影响其他类,它满足"开闭原则",对扩展比较灵活。
  • 可以实现宏命令:命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。

缺点

  • 使用命令模式可能会导致某些系统有过多的具体命令类:每个命令都需要一个具体的命令类,可能导致类的数量过多。
  • 系统结构更加复杂:引入了额外的命令类,增加了系统的复杂度。

使用场景

  • 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互:如GUI事件处理、网络请求处理等。
  • 系统需要在不同的时间指定请求、将请求排队和执行请求:如任务调度系统、消息队列系统等。
  • 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作:如文本编辑器、图形编辑器等。

最佳实践

  1. 合理设计命令接口:确保命令接口简洁明了,便于实现和维护
  2. 避免命令对象过重:不要在命令对象中放置过多的业务逻辑
  3. 考虑命令的撤销:在设计命令时,考虑如何实现撤销功能
  4. 使用命令队列:对于需要批量处理的场景,使用命令队列提高效率
  5. 记录命令日志:记录命令的执行过程,便于问题排查和审计

与其他模式的关系

  • 与备忘录模式结合:可以实现命令的撤销和重做功能
  • 与策略模式结合:可以为不同的命令类型提供不同的执行策略
  • 与观察者模式结合:可以通知其他对象命令的执行状态
  • 与组合模式结合:可以实现宏命令功能

更新历史

  • 2024-01-01: 完善命令模式文档,补充完整的代码实现示例和应用案例