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命令模式的资料请关注脚本之家其它相关文章!