在讨论命令模式之前,先来实现一个只有一个按钮的遥控器,这个遥控器的按钮要有能力控制电视和电灯:
ts
enum Status {
On,
Off,
}
class TV {
status = Status.Off;
turnOn = () => {
this.status = Status.On;
console.log("电视已打开");
};
turnOff = () => {
this.status = Status.Off;
console.log("电视已关闭");
};
}
enum Brightness {
Low,
High,
}
class Light {
brightness: Brightness;
open = () => {
if (this.brightness === undefined) {
this.setBrightness(Brightness.Low);
} else if (this.brightness === Brightness.Low) {
this.setBrightness(Brightness.High);
} else if (this.brightness === Brightness.High) {
this.setBrightness();
}
};
setBrightness = (brightness?: Brightness) => {
this.brightness = brightness;
if (this.brightness === undefined) {
console.log("电灯已关闭");
} else if (this.brightness === Brightness.Low) {
console.log("电灯已打开,亮度低");
} else if (this.brightness === Brightness.High) {
console.log("电灯已打开,亮度高");
}
};
}
class RemoteControl {
private undoStack: (Status | Brightness)[] = [];
private redoStack: (Status | Brightness)[] = [];
constructor(private tigger: Light | TV) {}
onButtonPressed = () => {
if (this.tigger instanceof Light) {
this.tigger.open();
this.undoStack.push(this.tigger.brightness);
} else if (this.tigger instanceof TV) {
if (this.tigger.status === Status.On) {
this.tigger.turnOff();
} else {
this.tigger.turnOn();
}
this.undoStack.push(this.tigger.status);
}
this.redoStack = []; // 清空重做栈,因为新命令改变了状态
};
private executeUndoCommand = (command: Status | Brightness) => {
if (this.tigger instanceof Light) {
if (command === Brightness.High) {
this.tigger.setBrightness(Brightness.Low);
} else if (command === Brightness.Low) {
this.tigger.setBrightness();
} else {
this.tigger.setBrightness(Brightness.High);
}
} else if (this.tigger instanceof TV) {
if (command === Status.Off) {
this.tigger.turnOn();
} else {
this.tigger.turnOff();
}
}
};
private executeRedoCommand = (command: Status | Brightness) => {
if (this.tigger instanceof Light) {
if (command === Brightness.High) {
this.tigger.setBrightness(Brightness.High);
} else if (command === Brightness.Low) {
this.tigger.setBrightness(Brightness.Low);
} else {
this.tigger.setBrightness();
}
} else if (this.tigger instanceof TV) {
if (command === Status.Off) {
this.tigger.turnOff();
} else {
this.tigger.turnOn();
}
}
};
undo = () => {
if (this.undoStack.length > 0) {
const command = this.undoStack.pop();
this.executeUndoCommand(command);
this.redoStack.push(command);
}
};
redo = () => {
if (this.redoStack.length > 0) {
const command = this.redoStack.pop();
this.executeRedoCommand(command);
this.undoStack.push(command);
}
};
}
//客户端代码
const testOperateTV = () => {
const tv = new TV();
const remote = new RemoteControl(tv);
remote.onButtonPressed(); // 输出:电视已打开
remote.onButtonPressed(); // 输出:电视已关闭
// 撤销最后一个命令(关闭电视)
remote.undo(); // 输出:电视已打开
// 重做撤销的命令
remote.redo(); // 输出:电视已关闭
};
testOperateTV();
//客户端代码
const testOperateLight = () => {
const light = new Light();
const remote = new RemoteControl(light);
remote.onButtonPressed(); // 输出:电灯已打开,亮度低
remote.onButtonPressed(); // 输出:电灯已打开,亮度高
remote.onButtonPressed(); // 输出:电灯已关闭
// 撤销最后一个命令(关闭电灯)
remote.undo(); // 输出:电灯已打开,亮度高
// 撤销最后倒数第二个命令(电灯已打开,亮度高)
remote.undo(); // 输出:电灯已打开,亮度低
// 重做撤销的命令
remote.redo(); // 输出:电灯已打开,亮度高
};
testOperateLight();
从上面的实现可以看到,当遥控器(RemoteControl)要能控制电视和电灯,就需要深入了解其细节,遥控器和被控制的对象紧紧耦合在一起。如果要接入其他被控制的电器,那么 RemoteControl 就会变得更加庞大与繁杂,耦合的对象越来越多,越来越难控制。而命令模式能很好的解决这一问题。
通过命令模式实现上述功能:
ts
//TV、Light代码同上
interface Command {
execute: () => void;
undo: () => void;
}
class RemoteControl {
private slot: Command;
private undoStack: Command[] = [];
private redoStack: Command[] = [];
setCommand = (command: Command) => {
this.slot = command;
};
onButtonPressed = () => {
this.slot.execute();
this.undoStack.push(this.slot);
this.redoStack = [];
};
undo = () => {
if (this.undoStack.length > 0) {
const command = this.undoStack.pop();
command.undo();
this.redoStack.push(command);
}
};
redo = () => {
if (this.redoStack.length > 0) {
const command = this.redoStack.pop();
command.execute();
this.undoStack.push(command);
}
};
}
class TurnOnTVCommand implements Command {
constructor(private tv: TV) {}
execute = () => {
this.tv.turnOn();
};
undo = () => {
this.tv.turnOff();
};
}
class TurnOffTVCommand implements Command {
constructor(private tv: TV) {}
execute = () => {
this.tv.turnOff();
};
undo = () => {
this.tv.turnOn();
};
}
//客户端代码
const testOperateTV = () => {
const tv = new TV();
const turnOnCommand = new TurnOnTVCommand(tv);
const turnOffCommand = new TurnOffTVCommand(tv);
const remote = new RemoteControl();
// 绑定命令
remote.setCommand(turnOnCommand);
remote.onButtonPressed(); // 输出:电视已打开
remote.setCommand(turnOffCommand);
remote.onButtonPressed(); // 输出:电视已关闭
// 撤销最后一个命令(关闭电视)
remote.undo(); // 输出:电视已打开
// 重做撤销的命令
remote.redo(); // 输出:电视已关闭
};
testOperateTV();
class LowBrightnessLightCommand implements Command {
constructor(private light: Light) {}
execute = () => {
this.light.setBrightness(Brightness.Low);
};
undo = () => {
this.light.setBrightness();
};
}
class HighBrightnessLightCommand implements Command {
constructor(private light: Light) {}
execute = () => {
this.light.setBrightness(Brightness.High);
};
undo = () => {
this.light.setBrightness(Brightness.Low);
};
}
class TurnOffLightCommand implements Command {
constructor(private light: Light) {}
execute = () => {
this.light.setBrightness();
};
undo = () => {
this.light.setBrightness(Brightness.High);
};
}
//客户端代码
const testOperateLight = () => {
const light = new Light();
const lowBrightnessCommand = new LowBrightnessLightCommand(light);
const highBrightnessCommand = new HighBrightnessLightCommand(light);
const turnOffCommand = new TurnOffLightCommand(light);
const remote = new RemoteControl();
remote.setCommand(lowBrightnessCommand);
remote.onButtonPressed(); // 输出:电灯已打开,亮度低
remote.setCommand(highBrightnessCommand);
remote.onButtonPressed(); // 输出:电灯已打开,亮度高
remote.setCommand(turnOffCommand);
remote.onButtonPressed(); // 输出:电灯已关闭
// 撤销最后一个命令(关闭电灯)
remote.undo(); // 输出:电灯已打开,亮度高
// 撤销最后倒数第二个命令(电灯已打开,亮度高)
remote.undo(); // 输出:电灯已打开,亮度低
// 重做撤销的命令
remote.redo(); // 输出:电灯已打开,亮度高
};
testOperateLight();
可以看到,通过命令模式实现遥控器,让遥控器得到了彻底的解耦,遥控器完全不需要知道要控制的电器是什么,它只需要知道在按钮按下的时候执行命令对象的 execute 方法。当需要控制其他电器时,也很简单,只需要实现相应的命令并绑定命令就可以了。 此外,命令模式实现 redo、undo 也非常简单。那什么是命令模式?
命令模式
命令模式(Command Pattern)是一种优雅的设计模式,它允许将各种操作(如请求、队列管理操作、日志记录操作等)封装成命令对象, 这个命令对象可以在程序中四处传递,并在晚些时候执行命令。通过这种方式,这些封装的操作(即命令对象)可以用作其他对象的参数。想象一下我们去餐厅吃饭的过程,顾客点菜,服务员拿到订单后将其放入后厨的订单栏排队(等同于通知厨师准备餐点),不久后厨师就能够完成炒菜,这个过程中,点单的顾客并清楚具体炒菜的的师傅是男是女,是胖是瘦,厨师和顾客间是互相无感知的。这就是命令模式,它通常涉及以下几个角色:
-
命令(Command)接口
定义执行操作的接口,通常会有一个 execute()方法用于执行命令。
tsinterface Command { execute: () => void; undo: () => void; }
-
具体命令(ConcreteCommand)类
实现命令接口(饭店里我们点的单-糖醋里脊,TurnOnTVCommand),并定义接收操作的绑定操作。具体命令类会有一个接收者(Receiver)对象,并调用接收者的功能来执行命令的具体操作。
-
接收者(Receiver)类
接收者(厨师-知道怎么炒菜,tv、light)知道怎么执行一个请求相关的操作。任何类都可能作为一个接收者。
-
调用者(Invoker)类
调用者(服务员,RemoteControl)通常会持有(服务员拿走订单,setCommand)命令对象,并在某个时间点调用命令对象的 execute()(通知厨师准备餐点, onButtonPressed)方法来提交请求。
-
客户(Client)类
客户(顾客,testOperateLight, testOperateTV)负责创建具体命令对象并设定它的接收者。
命令模式类图:
宏命令
宏命令(Macro Command)是命令模式的一个扩展,它允许将多个命令合并成一个命令,并一次执行它们,这在需要执行一系列操作时非常有用。比如,进门的时候希望打开电灯,打开电视:
ts
class MacroCommand implements Command {
private commands: Command[] = [];
addCommand = (command: Command) => {
this.commands.push(command);
};
execute = () => {
this.commands.forEach((command) => command.execute());
};
undo = () => {
this.commands
.slice()
.reverse()
.forEach((command) => command.undo());
};
}
const tv = new TV();
const tvOnCommand = new TurnOnTVCommand(tv);
const light = new Light();
const lightLowBrightnessCommand = new LowBrightnessLightCommand(light);
const macroCommand = new MacroCommand();
macroCommand.addCommand(tvOnCommand);
macroCommand.addCommand(lightLowBrightnessCommand);
macroCommand.execute(); //输出:电视已打开 电灯已打开,亮度低
macroCommand.undo(); //电灯已关闭 电视已关闭
通过宏命令就可以动态的组合命令,比较优雅。
重放
通过命令模式,实现重放功能就很简单,只需要记录命令栈,然后重新执行一遍就可以了:
ts
const makeCommand = (key: string) => {
const commands = {
j: {
execute: () => {
console.log("跳跃");
},
},
k: {
execute: () => {
console.log("击打");
},
},
l: {
execute: () => {
console.log("蹲下");
},
},
i: {
execute: () => {
console.log("防御");
},
},
};
return commands[key as keyof typeof commands];
};
class Man {
private commandStack: Command[] = [];
onKeyPressed = (code: string) => {
const command = makeCommand(code);
command.execute();
this.commandStack.push(command);
};
replay = () => {
if (this.commandStack.length > 0) {
let command;
while ((command = this.commandStack.shift())) {
command.execute();
}
}
};
}
const man = new Man();
man.onKeyPressed("i"); //输出:防御
man.onKeyPressed("k"); //输出:击打
man.onKeyPressed("j"); //输出:跳跃
man.onKeyPressed("l"); //输出:蹲下
man.replay(); //输出:防御 击打 跳跃 蹲下
队列请求
有时候需要对命令执行进行精细控制的场景,如任务调度、批处理作业等。以下实现一个异步任务调度,需要前一个 command 执行完成后才能执行下一个任务:
ts
interface Command {
execute: () => Promise<void>;
}
class Light {
on = async () => {
// 模拟异步操作
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log("Light is on");
};
off = async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log("Light is off");
};
}
class LightOnCommand implements Command {
constructor(private light: Light) {}
execute = async () => {
await this.light.on();
};
}
class LightOffCommand implements Command {
constructor(private light: Light) {}
execute = async () => {
await this.light.off();
};
}
class CommandQueue {
private queue: Command[] = [];
addCommand = (command: Command): void => {
this.queue.push(command);
};
executeCommands = async (): Promise<void> => {
for (const command of this.queue) {
await command.execute();
}
};
}
// 客户端代码
const light = new Light();
const lightOnCommand = new LightOnCommand(light);
const lightOffCommand = new LightOffCommand(light);
const commandQueue = new CommandQueue();
commandQueue.addCommand(lightOnCommand);
commandQueue.addCommand(lightOffCommand);
commandQueue
.executeCommands()
.then(() => console.log("All commands have been executed."));
日志请求
命令模式的日志请求特性非常适合于那些需要记录、审计、撤销/重做操作或者是后期操作重放的场景。 以下模拟实现了一个系统崩溃后可以恢复的文本编辑器:
ts
enum Operation {
Add,
Remove,
}
interface Command {
execute: () => void;
undo: () => void;
serialize: () => string;
}
class Editor {
text = "";
addText = (addition: string) => {
this.text += addition;
};
removeText = (length: number) => {
if (length < 0 || length > this.text.length) {
console.error("Invalid length for removeText.");
return;
}
this.text = this.text.substring(0, this.text.length - length);
};
}
class AddTextCommand implements Command {
constructor(private doc: Editor, private addition: string) {}
execute = () => {
this.doc.addText(this.addition);
};
undo = () => {
this.doc.removeText(this.addition.length);
};
serialize = () =>
JSON.stringify({ operation: Operation.Add, value: this.addition });
}
class RemoveTextCommand implements Command {
private removedText = "";
constructor(private doc: Editor, private length: number) {}
execute = () => {
this.removedText = this.doc.text.slice(-this.length);
this.doc.removeText(this.length);
};
undo = () => {
this.doc.addText(this.removedText);
};
serialize = () =>
JSON.stringify({ operation: Operation.Remove, value: this.length });
}
//本地存储
class DocumentStorage {
private snapshots: Snapshot[] = [];
private commandLog: string[] = [];
private lastSnapshotIndex = 0;
saveCommand = (command: Command) => {
this.commandLog.push(command.serialize());
};
createSnapshot = (doc: Editor) => {
this.snapshots.push({ timestamp: Date.now(), text: doc.text });
this.lastSnapshotIndex = this.commandLog.length; // Update the index to the current command log length
};
createCommandFromSerialized = (
doc: Editor,
serialized: string
): Command | null => {
const { operation, value } = JSON.parse(serialized) as {
operation: Operation;
value: any;
};
switch (operation) {
case Operation.Add:
return new AddTextCommand(doc, value);
case Operation.Remove:
const length = parseInt(value, 10);
return new RemoveTextCommand(doc, length);
default:
return null;
}
};
recoverDocument = (doc: Editor) => {
const latestSnapshot = this.snapshots.pop();
if (latestSnapshot) {
doc.text = latestSnapshot.text;
const commandsToReapply = this.commandLog.slice(this.lastSnapshotIndex);
commandsToReapply.forEach((commandText) => {
const command = this.createCommandFromSerialized(doc, commandText);
command.execute();
});
}
};
}
interface Snapshot {
timestamp: number;
text: string;
}
class CommandInvoker {
constructor(private doc: Editor, private documentStorage: DocumentStorage) {}
executeCommand = (command: Command) => {
command.execute();
this.documentStorage.saveCommand(command);
};
}
// 使用示例
const doc = new Editor();
const storage = new DocumentStorage();
const invoker = new CommandInvoker(doc, storage);
invoker.executeCommand(new AddTextCommand(doc, "你"));
invoker.executeCommand(new AddTextCommand(doc, "好吗"));
console.log("Recovered Editor Text: ", doc.text); //输出:你好吗
// 创建快照
storage.createSnapshot(doc);
console.log("Recovered Editor Text: ", doc.text); //输出:你好吗
invoker.executeCommand(new RemoveTextCommand(doc, 1));
console.log("Recovered Editor Text: ", doc.text); //输出:你好
// 假设现在系统重启,需要从最后的快照和日志中恢复, 备份的快照为:你好吗,需要恢复删除操作后的内容,即你好
storage.recoverDocument(doc);
console.log("Recovered Editor Text: ", doc.text); //输出:你好
命令模式的优点
-
解耦发送者和接收者
命令模式通过命令对象来分离请求的发起者和请求的执行者。发送者只知道如何发送命令,而不需要知道命令的具体实现细节。
-
扩展性
新的命令可以很容易地加入到系统中,因为发送者的代码不需要改变。这符合开闭原则,对扩展开放,对修改封闭。
-
复合命令
可以将多个命令组合成一个复合命令(也称为宏命令),这样就可以批量执行多个操作。
-
可撤销操作
命令模式可以实现命令的撤销和重做,这是因为每个命令都有执行操作的具体实现。通过存储历史命令,可以轻松地回到之前的状态。
-
更好的控制逻辑
通过命令对象,可以更灵活地控制操作的执行,比如延迟执行、排队执行、日志记录等。
命令模式的缺点
-
类的增加
对每个操作或请求,都需要创建一个具体的命令类,这会使系统中的类的数量增加,增加了系统的复杂性。
-
增加了抽象层次
引入命令模式会增加系统的抽象层次,有时可能会使得系统的理解和调试变得更加困难。
-
性能问题
如果命令非常频繁或在资源受限的系统中使用,创建大量的命令对象可能会影响性能。
总的来说,命令模式提供了显著的灵活性和扩展性,尤其适用于需要命令队列、日志和撤销操作的场景。