“小度,小度,热死了请打开空调~”,“小度,小度,主人回家了~”,“小度,小度,播放一首炸雷~”,像小度类似的智能产品正在逐步改善我们的生活,只要我们一声令下,产品立马执行,毫不拖延。就如同老板的命令一样,系统需接收到命令后经过一系列分析后,会自动选择执行哪些命令,而发令者完全不关心内部是执行了哪些动作以及哪个具体方法。比如“小度的主人回家了”,小度内部接受到指令后会自动分析出需要打开灯光,打开窗帘,打开中央空调,播放一首著名的萨克斯独奏-回家以及检查房门是否已关闭等内容。这一切都是不需要发令者感知的 ,发令者只需要发出指令,具体该执行哪些功能由指令控制器来分析完成。
对于这样的场景下,如何从代码层面上很好的解决呢?我的第一个想法那就是封装,面向对象那些事情都离不开封装。封装系统向外透出的功能,我们很自然能够想到“接口”、“API”这些概念。发送者发送的指令可以是由系统提供一系列API,比如电商系统的提单就调用类似/order/submit的API,加入购物车就调用/cart/add/product/的API。这些API的具体执行逻辑在调用方也是解耦(不感知)的,其内部流程化逻辑也是多个系统功能之间的紧密配合。但尽管如此,API或接口的这种方式依然不能够解决“小度”类似场景的问题。为何呢?这是因为小度的指令执行不是流程化的东西,具体的说就是一条指令可能是“打开空调”,也可能是“打开空调,打开灯光”,也可能是“打开灯光,关闭窗帘”,这种功能之间的组合是非流程化的东西,无法穷尽就不适合通过API、接口进行封装。更甚者我如果说“小度,小度,撤销上一次的操作”,我想问问你使用API、接口能完成这项工作吗?明显不能。但是我们今天要讲述的**命令模式(Command Pattern)**就可以。
一、命令模式的概念
命令模式,又称作动作模式或事务模式。其一般定义(稍有改动)如下:
将请求封装为命令对象,使得客户端对系统的请求参数化、具体请求可以在运行时更改、排队、记录,并且能够提供命令的撤销和恢复功能。
命令模式定义中,关于第一句我个人认为有思考的空间。我认为封装的不应该是客户端请求,而是系统所提供的一系列可执行业务逻辑,比如上文中“灯光控制逻辑”、“窗帘控制逻辑”等。封装客户端请求注定会带来很多扩展性问题,并且最关键的是你根本无法预知客户端会如何请求,以及请求哪些功能。最后带来的问题必然是无法支持客户端变化无端的需要,需要不端增加对应的组合命令对象,引起类的剧增。所以,我认为应该将可执行单元封装为命令对象。
系统所提供的的功能向外透出为一系列的可执行的命令对象,而客户端只需要通过参数告知系统应该执行哪些命令,或通过其他可解析参数的方式(由系统解析为指令)来完成请求即可。
既然系统功能已经封装为一个个命令(Command )组成,那么这些命令自然就涉及到运行时更改(全部取消或暂停执行)、排队(多个未执行命令排队顺序执行)、记录(记录下历史已执行的命令)、撤销(取消上一次执行的命令)、恢复(恢复上一次执行的命令)等操作。那这部分职责就应该由命令执行器(Command Executor)来负责。
概念总结:
- 命令模式能够解决客户端多样化功能组合请求,且需具备事务回滚处理能力的场景。
- 命令模式将系统功能拆分独立命令,客户端发出命令来执行其所需要的功能
- 命令模式将系统功能逻辑及客户端请求之间进行了解耦。Command仅需关注本身业务逻辑的实现,而客户端解析命令以及命令之间的相关调配由CommandExecutor负责。
二、应用实践
为了更好的理解、应用命令模式,本章将给出一个能够使用命令模式解决相关问题的案例。
案例背景就是关于上文中提到的小度智能家居的例子。小度系统涉及的功能包括开关机控制模块、灯光控制模块,窗帘控制模块,以及门锁检查模块(为简化代码,其他功能暂不考虑)。为了更好处理主人的命令,小度系统还需要提供一个智能控制器来负责命令之间的排队、撤销、恢复、解析等功能。
命令接口:
public interface Command {
// 执行当前命令
void execute();
// 恢复当前命令--命令需具备回滚能力
void undo();
}
打开灯光命令:
public class TurnOnLightCommand implements Command {
public static final TurnOnLightCommand turnOnLightCommandIns = new TurnOnLightCommand();
private final LightControl lightControl;
public TurnOnLightCommand() {
this.lightControl = new LightControl();
}
@Override
public void execute() {
lightControl.turnOn();
}
@Override
public void undo() {
lightControl.turnOff();
}
}
关闭灯光命令:
public class TurnOffLightCommand implements Command {
public static final TurnOffLightCommand turnOffLightCommandIns = new TurnOffLightCommand();
private final LightControl lightControl;
public TurnOffLightCommand() {
this.lightControl = new LightControl();
}
@Override
public void execute() {
lightControl.turnOff();
}
@Override
public void undo() {
lightControl.turnOn();
}
}
打开窗帘命令:
public class OpenCurtainCommand implements Command {
public static final OpenCurtainCommand openCurtainCommandIns = new OpenCurtainCommand();
private final CurtainControl curtainControl;
public OpenCurtainCommand() {
this.curtainControl = new CurtainControl();
}
@Override
public void execute() {
curtainControl.open();
}
@Override
public void undo() {
curtainControl.close();
}
}
关闭窗帘命令:
public class CloseCurtainCommand implements Command {
public static final CloseCurtainCommand closeCurtainCommandIns = new CloseCurtainCommand();
private final CurtainControl curtainControl;
public CloseCurtainCommand() {
this.curtainControl = new CurtainControl();
}
@Override
public void execute() {
curtainControl.close();
}
@Override
public void undo() {
curtainControl.open();
}
}
智能家居控制器代码实现:
/**
* 智能家居控制系统
*/
public class SmartHomeControl {
private final Queue<Command> commandQueue; // 命令队列
public SmartHomeControl() {
commandQueue = new LinkedList<>();
}
public void request(String strCommand) {
// 先解析出命令列表
List<Command> commands = parseCommand(strCommand);
this.request(commands);
}
public void request(List<Command> commands) {
if(commands == null || commands.isEmpty()) {
return;
}
commandQueue.addAll(commands); // 添加到队列中等待执行
}
public void executorCommands() { // 逐个执行队列中的命令
while (!commandQueue.isEmpty()) {
Command command = commandQueue.poll();
command.execute();
}
}
public void undorCommands() { // 逐个取消队列中的命令
while (!commandQueue.isEmpty()) {
Command command = commandQueue.poll();
command.undo();
}
}
private List<Command> parseCommand(String strCommand) {
// 根据strCommand解析出对应的指令列表
return new ArrayList<>();
}
}
客户端执行打开灯光,和恢复指令操作:
public class Client {
public static void main(String[] args) {
// 创建智能家居模块
SmartHomeControl smartHomeControl = new SmartHomeControl();
// 发出打开窗帘命令
Command turnOnLightCommand = TurnOnLightCommand.turnOnLightCommandIns;
smartHomeControl.request(Collections.singletonList(turnOnLightCommand));
smartHomeControl.executorCommands(); // 执行命令
smartHomeControl.request(Collections.singletonList(turnOnLightCommand));
smartHomeControl.undorCommands(); // 执行取消命令
}
}
从代码及类图上可以看出,通过将各个业务逻辑单元封装为一个个Command类,由SmartHomeControl类来负责命令之间的入队、执行等操作,实现了命令调用者与实际业务逻辑之间的解耦合,提高了重用的方法。客户端可以任意选择执行不同命令的组合。
命令模式的优点:
- 将调用者与具体执行者解耦
- 支持将多个命令装配为一个复合命令
- 支持操作的事务能力
- 通过命令控制器可以做很多东西,比如限流、拦截器、校验以及日志记录等等
- 符合开闭原则,新增命令很容易,不会影响原有功能
命令模式的缺点:
- 系统功能逐步复杂时,命令类可能会很多。