命令模式

在讨论命令模式之前,先来实现一个只有一个按钮的遥控器,这个遥控器的按钮要有能力控制电视和电灯:

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()方法用于执行命令。

    ts 复制代码
    interface 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); //输出:你好

命令模式的优点

  • 解耦发送者和接收者

    命令模式通过命令对象来分离请求的发起者和请求的执行者。发送者只知道如何发送命令,而不需要知道命令的具体实现细节。

  • 扩展性

    新的命令可以很容易地加入到系统中,因为发送者的代码不需要改变。这符合开闭原则,对扩展开放,对修改封闭。

  • 复合命令

    可以将多个命令组合成一个复合命令(也称为宏命令),这样就可以批量执行多个操作。

  • 可撤销操作

    命令模式可以实现命令的撤销和重做,这是因为每个命令都有执行操作的具体实现。通过存储历史命令,可以轻松地回到之前的状态。

  • 更好的控制逻辑

    通过命令对象,可以更灵活地控制操作的执行,比如延迟执行、排队执行、日志记录等。

命令模式的缺点

  • 类的增加

    对每个操作或请求,都需要创建一个具体的命令类,这会使系统中的类的数量增加,增加了系统的复杂性。

  • 增加了抽象层次

    引入命令模式会增加系统的抽象层次,有时可能会使得系统的理解和调试变得更加困难。

  • 性能问题

    如果命令非常频繁或在资源受限的系统中使用,创建大量的命令对象可能会影响性能。

总的来说,命令模式提供了显著的灵活性和扩展性,尤其适用于需要命令队列、日志和撤销操作的场景。

相关推荐
崔庆才丨静觅9 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606110 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了10 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅10 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅10 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅11 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment11 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅11 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊11 小时前
jwt介绍
前端
爱敲代码的小鱼11 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax