JS设计模式之命令模式的用法详解

 更新时间:2023年08月27日 10:07:50   作者:慕仲卿  
JavaScript中的命令模式是一种设计模式,它提供了一种将命令封装为对象的方式,从而允许我们将请求与实际执行该请求的操作对象解耦,这种模式可以在不同的场景中使用,例如实现撤销/重做操作、队列任务等,本文我们将讲解命令设计模式在JS中的使用

相关定义:

  • 使用命令模式,可以将【请求的调用者】和【请求的执行者】解耦。

  • 调用者通过【持有命令对象】来【间接调用】接收者的方法,而无需【直接引用】接收者或了解其【具体实现】。

  • 这种解耦使得我们能够更加灵活地【扩展】和【改变】命令的调用方式。

  • 例如,我们可以【将命令对象保存在【队列中】】,实现命令的【排队】和【异步执行】。

  • 还可以记录命令的【历史,以支持撤销和重做】操作。

  • 命令模式应用场景,【菜单操作】、【多级撤销】、【批处理任务】等。

自我理解:

  • 执行者对象E:普通对象,提供了整个流程中的执行方法,此方法对于具体的命令是可感知的,对于调用者是无感知的!

  • 抽象命令类A:规定了具体命令的基本结构,为调用者提供统一的调用接口。

  • 具体命令类S:是对抽象命令类的实现,核心是封装了执行者,准确来说命令只封装了执行者的部分方法;抽象方法的执行本质上是执行了这些方法。

  • 调用者对象C:封装了两个方法和一个属性;属性表示的是当前命令,第一个方法是设置/切换命令具体值,第二个方法是执行此方法。

  • S封装了E,C只能接触到S,所以C和E是解耦的

  • A和S的关系是一对多

解耦体现:

  • C无需了解S具体内容,只需完成触发 ;

  • S无需知道E怎么完成任务,只需调用E暴露出来的方法即可;

  • S和E之间是多对多的关系,即一个S可以由多个E的部分方法组合完成,一个E也可以被不同的S封装其上不同的部分方法;

  • C维护的不只是一个S,还可以是一个S队列,当触发到来的时候,S队列依次执行。

代码举例1:

// 接收者对象
class Light {
    turnOn() {}
    turnOff() {}
}
// 命令接口
abstract class Command {
    abstract execute():void;
}
// 具体命令:打开灯
class TurnOnCommand extends Command {
    constructor(public light: Light) { super() }
    execute() { this.light.turnOn() }
}
// 具体命令:关闭灯
class TurnOffCommand extends Command {
    constructor(public light: Light) { super() }
    execute() { this.light.turnOff() }
}
// 调用者对象
class RemoteControl {
    command: Command;
    setCommand(command: Command) { this.command = command }
    pressButton() { this.command.execute() }
}
// 使用示例
// 创建执行者
const light = new Light();
// 创建具体命令对象,封装执行者
const turnOnCommand = new TurnOnCommand(light);
const turnOffCommand = new TurnOffCommand(light);
// 创建调用者对象
const remoteControl = new RemoteControl();
// 设置具体命令对象然后执行
remoteControl.setCommand(turnOnCommand);
remoteControl.pressButton();

代码举例2:

使用命令设计模式可以灵活组合网络请求,如下所示:

// 请求接收者
class Reciever {
  async get(path: string) {
    const res = await fetch(`https://rec.example.com/${path}`);
    const data = await res.json();
  }
}
// 命令接口
class Cmd { exe() {} }
// 具体命令:发送请求
class RCd extends Cmd {
  constructor(public rec, public url) {
    super();
  }
  exe() {
    return this.rec.get(this.url);
  }
}
// 调用者对象
class RM {
  cQueue: Cmd[] = [];
  add(command: Cmd) {
    this.cQueue.push(command);
  }
  pReq() {
    const promises = this.cQueue.map((command) => command.exe());
    return Promise.all(promises);
  }
}
// 使用示例
const rec = new Reciever(); // 创建请求接收者
const rM = new RM(); // 创建请求管理者
// 添加具体请求命令
rM.add(new RCd(rec, 'data1'));
rM.add(new RCd(rec, 'data2'));
rM.add(new RCd(rec, 'data3'));
rM.pReq()
  .then(() => {
    console.log('所有请求已完成');
  })
  .catch((error) => {
    console.error('请求出错:', error);
  });

代码举例3:

使用命令设计模式实现撤销和重做,用到了栈数据结构,如下所示:

// 命令接口:想要用命令策略实现撤销、重做就必须先在抽象接口中定义好撤销的接口
abstract class Cmd {
  abstract exe(): void;
  abstract undo(): void;
}
// 具体命令类 - 加法命令
class AddCmd extends Cmd {
  constructor(public rec: Rec, public value: number) {
    super();
  }
  exe() {
    this.rec.add(this.value);
  }
  undo() {
    this.rec.subtract(this.value);
  }
}
// 接收者类
class Rec {
  result = 0;
  add(value: number) { this.result += value }
  subtract(value: number) { this.result -= value }
}
// 调用者/发送者
class Invoker {
  cmds: Cmd[] = [];
  xcmd: Cmd[] = [];
  exe(cmd: Cmd) {
    cmd.exe();
    this.cmds.push(cmd);
  }
  // 重点
  undo() {
    const cmd = this.cmds.pop();
    if (!cmd) return;
    cmd.undo();
    this.xcmd.push(cmd);
  }
  // 重点
  redo() {
    const cmd = this.xcmd.pop();
    if (cmd) {
      cmd.exe();
      this.cmds.push(cmd);
    }
  }
}
// 示例用法
const rec = new Rec(); // 创建接收者对象
const ivk = new Invoker(); // 创建调用者对象
const addCmd = new AddCmd(rec, 5); // 创建加法命令
ivk.exe(addCmd); // 执行加法命令,结果为:5
ivk.undo(); // rec.result = 0
ivk.redo(); // rec.result = 5

命令设计模式和策略设计模式的不同:

  • 命令设计模式的最小操作单元是【命令对象】;而策略设计模式的最小操作单元是方法,或者算法。

  • 命令设计模式一次只操作一个命令对象;而策略设计模式为了完成任务可以组合多个策略。

  • 命令设计模式一般不会将某个命令单独保存到内部状态中;而策略设计模式必须保存当前的策略。

  • 使用命令设计模式可以实现撤销、重做等功能、这反映出各个命令之间是平等关系;而策略设计模式的各个策略之间可能是先后顺序关系。

原生使用

下面这些是 JavaScript 中常见的原生部分,它们在某种程度上使用到了命令模式的思想和机制。通过封装行为成具体的对象并在需要时进行调用,这些原生功能可以提供更灵活、可扩展的方式来处理相关的请求或操作。

  • 事件处理:JavaScript 中的事件处理机制可以看作是一种命令模式的应用。当用户与页面进行交互时,例如点击按钮、键盘按键或鼠标移动等,事件被触发并执行相应的处理函数。这里事件就充当了命令对象,而事件处理函数则扮演着命令的接收者。

  • XMLHttpRequest 对象:在早期的 Ajax 开发中,我们常使用 XMLHttpRequest 对象来进行异步请求。开发者可以将每个请求封装成一个对象,并通过调用 send() 方法来发送请求。这里的 XMLHttpRequest 对象和 send() 方法即可看作是命令模式的实现,发送请求的行为被封装成具体的命令对象。

  • History API:浏览器的 History API 提供了对浏览器历史记录的控制。通过调用 pushState() 或 replaceState() 方法,我们可以添加或替换浏览器的历史记录条目,并关联相应的状态数据。这里的 pushState() 和 replaceState() 方法可以看作是命令对象,用于执行添加或替换历史记录的操作。

  • document.execCommand():Document 对象的 execCommand() 方法允许在网页中执行命令式的编辑操作,如粘贴、剪切、加粗、斜体等。开发者可以调用 execCommand() 方法并传递相应的命令参数来执行这些操作,从而实现富文本编辑功能。

  • setTimeout() 和 setInterval():JavaScript 提供了 setTimeout() 和 setInterval() 函数来实现定时器功能。开发者可以使用这两个函数将一段代码封装成一个函数对象,并在指定的时间间隔后执行相应的代码,相当于将定时器行为封装成具体的命令对象。

业务实践:

  • 按钮和用户交互:当你需要实现一个具有撤销、重做或记录操作历史的按钮交互功能时,可以使用命令模式。每个按钮可以表示一个命令对象,按下按钮时执行相应的命令操作。

也就是说但凡见到按钮,都可以使用命令设计模式。

  • 异步请求管理:当你需要对异步请求进行批处理、队列化或延迟执行时,命令模式可以很好地组织和管理这些请求。将每个请求封装成一个命令对象,并使用命令队列来依次执行这些命令。

  • 菜单和快捷键:当你需要实现复杂的菜单系统或支持快捷键操作时,命令模式可以帮助你处理不同的菜单项或快捷键动作。每个菜单项或快捷键可以关联一个命令对象,触发时执行相应的命令操作。

  • 动画控制:当你需要控制页面元素的复杂动画序列或状态切换时,命令模式可以提供一种有效的方式。每个动画或状态切换可以封装成一个命令对象,通过调用者来触发执行。

  • 历史记录与撤销:当你需要实现撤销和重做功能或记录用户操作历史时,命令模式非常有用。每个用户操作可以表示一个命令对象,并在执行时更新状态或记录操作,以便支持撤销和重做操作。

以上就是JS设计模式之命令模式的用法详解的详细内容,更多关于JS命令模式的资料请关注脚本之家其它相关文章!

相关文章

最新评论