🔥 核心
命令模式即发送者-命令-接收者的结构。
这替代了发送者对接收者的直接调用。
🙁 问题场景
在市中心逛了很久的街后,你找到了一家不错的餐厅。
这家餐厅不仅装修简约朴素,在对员工的雇佣上也是极为精简——这里连个服务员都没有,只有厨师一个人在厨房里闷头做菜。
美味的食物才是餐厅的灵魂,你这么安慰着自己。你瞧了瞧墙上贴着的菜单,嗯嗯,油焖茄子看样子非常美味。于是你亲自来到厨房,告诉厨师你要点一份油焖茄子。
回到座位后,你觉得想再来点肉类,于是你又亲自来到了厨房,告诉师傅你再点一份红烧小龙虾。
再次回到座位,你突然又发现自己忘了点主食…你实在不想再亲自跑一趟了!
🙂 解决方案
几近崩溃的你想到了一个好主意。
你掏出手机,添加了厨师的微信。每当你想要点菜加菜退菜时,你就不必亲力亲为,去厨房找厨师;而是将自己的需求编辑为一条微信信息,发送到厨师的手机上;厨师看一眼微信消息队列,从中取出一个需求,并开始执行做菜动作。
这就是命令模式,通俗易懂,就是将「直接调用」改进为了「发送者-命令-接收者」的结构。
🌈 有趣的例子
在 股票交易平台(Platform)
,你可以直接调用 股票(Stock)
对象,进行 买进(buy())
和 售出(sell())
的动作。
我们使用命令模式,对这种直接调用进行改进。在 股票交易平台
上,通过一个个 命令(Order)
,实现对股票行为的操控。
股票(实际执行者)
class Stock {
private String name = "A";
private int price = 10000;
public void buy() {
info();
System.out.println("买进!");
}
public void sell() {
info();
System.out.println("售出!");
}
private void info() {
StringBuilder builder = new StringBuilder();
builder.append("【").append(name).append("股】");
builder.append("【").append(price).append("元】");
System.out.print(builder.toString());
}
}
命令(接口)
interface Order {
void execute();
}
买进命令
class BuyOrder implements Order {
// 引用到真正的执行者
private Stock stock;
public BuyOrder(Stock stock) {
this.stock = stock;
}
@Override
public void execute() {
stock.buy();
}
}
售出命令
class SellOrder implements Order {
// 引用到真正的执行者
private Stock stock;
public SellOrder(Stock stock) {
this.stock = stock;
}
@Override
public void execute() {
stock.sell();
}
}
股票交易平台(命令发送者兼命令队列)
class Platform {
// 命令队列
private List<Order> orders = new ArrayList<>();
// 向命令队列中添加命令
public void prepareOrder(Order order) {
orders.add(order);
}
// 执行命令队列所有命令
public void completeOrder() {
for (Order order : orders) {
order.execute();
}
orders.clear();
}
}
public class CommandPatternDemo {
public static void main(String[] args) {
// 新建一支股票
Stock stock = new Stock();
// 两个买进命令
BuyOrder buyOrder1 = new BuyOrder(stock);
BuyOrder buyOrder2 = new BuyOrder(stock);
// 一个售出命令
SellOrder sellOrder = new SellOrder(stock);
// 使用股票交易平台操控命令
Platform platform = new Platform();
platform.prepareOrder(buyOrder1);
platform.prepareOrder(buyOrder2);
platform.prepareOrder(sellOrder);
platform.completeOrder();
}
}
【A股】【10000元】买进!
【A股】【10000元】买进!
【A股】【10000元】售出!
☘️ 使用场景
◾️如果你需要通过操作来参数化对象,可使用命令模式。
命令模式可将特定的方法调用转化为独立对象。这一改变也带来了许多有趣的应用:你可以将命令作为方法的参数进行传递、将命令保存在其他对象中,或者在运行时切换已连接的命令等。
举个例子:你正在开发一个 GUI 组件(例如上下文菜单),你希望用户能够配置菜单项,并在点击菜单项时触发操作。
◾️如果你想要将操作放入队列中、操作的执行或者远程执行操作,可使用命令模式。
同其他对象一样,命令也可以实现序列化(序列化的意思是转化为字符串),从而能方便地写入文件或数据库中。一段时间后,该字符串可被恢复成为最初的命令对象。因此,你可以延迟或计划命令的执行。但其功能远不止如此!使用同样的方式,你还可以将命令放入队列、记录命令或者通过网络发送命令。
◾️如果你想要实现操作回滚功能,可使用命令模式。
尽管有很多方法可以实现撤销和恢复功能,但命令模式可能是其中最常用的一种。
为了能够回滚操作,你需要实现已执行操作的历史记录功能。命令历史记录是一种包含所有已执行命令对象及其相关程序状态备份的栈结构。
这种方法有两个缺点。首先,程序状态的保存功能并不容易实现,因为部分状态可能是私有的。你可以使用备忘录模式来在一定程度上解决这个问题。
其次,备份状态可能会占用大量内存。因此,有时你需要借助另一种实现方式:命令无需恢复原始状态,而是执行反向操作。反向操作也有代价:它可能会很难甚至是无法实现。
🧊 实现方式
(1)声明仅有一个执行方法的命令接口。
(2)抽取请求并使之成为实现命令接口的具体命令类。每个类都必须有一组成员变量来保存请求参数和对于实际接收者对象的引用。所有这些变量的数值都必须通过命令构造函数进行初始化。
(3)找到担任发送者职责的类。在这些类中添加保存命令的成员变量。发送者只能通过命令接口与其命令进行交互。发送者自身通常并不创建命令对象,而是通过客户端代码获取。
(4)修改发送者使其执行命令,而非直接将请求发送给接收者。
(5)客户端必须按照以下顺序来初始化对象:
- 创建接收者。
- 创建命令,如有需要可将其关联至接收者。
- 创建发送者并将其与特定命令关联。
🎲 优缺点
➕ 你可以实现撤销和恢复功能。
➕ 你可以实现操作的延迟执行。
➕ 你可以将一组简单命令组合成一个复杂命令。
➕ 单一职责原则。你可以解耦触发和执行操作的类。
➕ 开闭原则。你可以在不修改已有客户端代码的情况下在程序中创建新的命令。
➖ 某些系统有过多的具体命令类。
➖ 代码可能会变得更加复杂, 因为你在发送者和接收者之间增加了一个全新的层次。
🌸 补充
命令模式和消息队列(MQ)的思想有些许类似。