命令模式

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

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); //输出:你好

命令模式的优点

  • 解耦发送者和接收者

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

  • 扩展性

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

  • 复合命令

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

  • 可撤销操作

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

  • 更好的控制逻辑

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

命令模式的缺点

  • 类的增加

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

  • 增加了抽象层次

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

  • 性能问题

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

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

相关推荐
Justinc.3 分钟前
CSS3新增边框属性(五)
前端·css·css3
neter.asia19 分钟前
vue中如何关闭eslint检测?
前端·javascript·vue.js
~甲壳虫20 分钟前
说说webpack中常见的Plugin?解决了什么问题?
前端·webpack·node.js
光影少年39 分钟前
vue2与vue3的全局通信插件,如何实现自定义的插件
前端·javascript·vue.js
As977_40 分钟前
前端学习Day12 CSS盒子的定位(相对定位篇“附练习”)
前端·css·学习
susu108301891142 分钟前
vue3 css的样式如果background没有,如何覆盖有background的样式
前端·css
Ocean☾44 分钟前
前端基础-html-注册界面
前端·算法·html
Dragon Wu1 小时前
前端 Canvas 绘画 总结
前端
CodeToGym1 小时前
Webpack性能优化指南:从构建到部署的全方位策略
前端·webpack·性能优化
~甲壳虫1 小时前
说说webpack中常见的Loader?解决了什么问题?
前端·webpack·node.js