命令模式
定义
命令模式(command pattern):命令模式将请求(命令)封装为一个对象,这样可以使用不同的请求参数化其它对象(将不同请求依赖注入到其它对象),并且能够支持请求(命令)的排队执行、记录日志、撤销等(附加控制)功能
命令模式的核心是将指令信息封装成一个对象,并将此对象作为参数发送给接收方去执行,达到使命令的请求与执行方解耦,双方只通过传递各种命令对象来完成任务。
在实际的开发中,如果用到的编程语言并不支持用函数作为参数来传递,那么就可以借助命令模式将函数封装为对象来使用。
提示
C 语言支持函数指针,我们可以把函数当作变量传递来传递去。但是,在大部分编程语言中,函数没法作为参数传递给其它函数,也没法赋值给变量。借助命令模式,我们可以将函数封装成对象。具体来说就是,设计一个包含这个函数的类,实例化一个对象传来传去,这样就可以实现把函数像对象一样使用。
模式原理
- Command:抽象的命令类,定义命令的接口。
- ConcreteCommand:具体命令角色,通常会持有接收者的引用,并调用接收者的功能来完成命令要执行的相应的功能。
- Receiver(接收者):真正执行命令的对象。
- Invoker(调用者):持有命令对象的引用,调用命令对象来执行请求。
设计要点
- 解耦请求与执行:通过命令对象将请求 发送者与请求执行者解耦
- 支持队列操作:可以轻松实现命令的排队执行
- 支持撤销操作:可以保存命令历史,实现撤销功能
- 支持日志记录:可以记录命令的执行过程
应用案例
模拟酒店后厨的出餐流程,来对命令模式进行演示,命令模式的角色与案例中的角色对应关系如下:
- 服务员:即调用者角色,由它来发起命令。
- 厨师:接收者,真正执行命令的对象
- 订单:命令中包含订单。
核心类实现
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)操作:如文本编辑器、图形编辑器等。
最佳实践
- 合理设计命令接口:确保命令接口简洁明了,便于实现和维护
- 避免命令对象过重:不要在命令对象中放置过多的业务逻辑
- 考虑命令的撤销:在设计命令时,考虑如何实现撤销功能
- 使用命令队列:对于需要批量处理的场景,使用命令队列提高效率
- 记录命令日志:记录命令的执行过程,便于问题排查和审计
与其他模式的关系
- 与备忘录模式结合:可以实现命令的撤销和重做功能
- 与策略模式结合:可以为不同的命令类型提供不同的执行策略
- 与观察者模式结合:可以通知其他对象命令的执行状态
- 与组合模式结合:可以实现宏命令功能