深入理解JavaScript设计模式之命令模式
文章目录
- 深入理解JavaScript设计模式之命令模式
- 定义
- 简单命令模式
- 组合命令模式
- 使用命令模式实现文本编辑器
- 目标
- 关键类说明
- 实现的效果
- 交互逻辑流程
- 所有代码:
- 总结
定义
命令模式也是设计模式种相对于变焦简单容易理解的一种设计模式。
在
JavaScript
中,命令模式用于将一个请求或简单操作封装为一个对象。这使得你可以使用不同的请求、队列请求或者记录请求日志、撤销操作等。命令模式通常用于实现诸如撤销/重做功能、事务系统以及在复杂对象间传递请求等场景。
白话说就是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。使得请求发送者和请求接收者能够消除彼此之间的耦合关系,命令模式还支持撤销、排队等操作。
简单命令模式
定义很难了解命令模式的用处,举个开灯关灯的命令模式例子,如下,定义了两个LightOnCommand
与LightOffCommand
两个命令分别执行light
对象中的on
与off
方法,在new LightOnCommand
的时候将light作为参数传入并执行LightOnCommand.execute();
的方法实现开灯关灯的操作。
<body><button id="btn">按钮</button>
</body>
<script>/*** 点击按钮,执行开灯关灯的操作*/const light = {on() {console.log("开灯");},off() {console.log("关灯");},};class LightOnCommand {constructor(light) {this.light = light;}execute() {this.light.on();}}class LightOffCommand {constructor(light) {this.light = light;}execute() {this.light.off();}}const onLight = new LightOnCommand(light);const offLight = new LightOffCommand(light);let isOn = true;document.getElementById("btn").addEventListener("click", function () {(isOn ? onLight : offLight).execute();isOn = !isOn;});
</script>
一开始学的时候觉得这就是脱裤子放屁多此一举,但是仔细还差与思考,使用这种方式,可以让代码更加模块化与更容易维护。
- 解耦:调用者和接收者之间解耦,调用者不需要知道接收者的具体实现。
- 扩展性:可以很容易地添加新的命令而不需要修改现有的类。
- 可撤销操作:可以通过记录命令的历史来实现撤销操作。
- 队列请求:可以将命令存储在队列中,按顺序执行。
- 日志记录:可以记录命令的历史,便于调试和回溯。
组合命令模式
第一个简单的例子可以看到点击按钮只执行了一次命令,如果有多条命令,那就可以将多个命令添加到Command
里的一个stack
数组中,最后执行Command.execute
的时候遍历stack
数组中的命令统一遍历执行。
页面中有一个按钮 #btn
,当点击按钮时,依次执行以下三个命令:
- 开灯(LightOnCommand)
- 工人开始工作并停止(WorkerCommand)
- 关灯(LightOffCommand)
这些命令被添加到一个 Command
对象中,并在点击事件发生时统一执行,代码如下:
<body><button id="btn">按钮</button>
</body>
<script>class Command {constructor() {this.stack = [];}add(command) {this.stack.push(command);}execute() {this.stack.forEach((command) => command.execute());}}const light = {on: () => console.log("开灯"),off: () => console.log("关灯"),};const worker = {do: () => console.log("开始工作"),stop: () => console.log("停止工作"),};class WorkerCommand {constructor(worker) {this.worker = worker;}execute() {this.worker.do();this.worker.stop();}}// 命令拆分class LightOnCommand {constructor(light) {this.light = light;}execute() {this.light.on();}}class LightOffCommand {constructor(light) {this.light = light;}execute() {this.light.off();}}const command = new Command();command.add(new LightOnCommand(light));command.add(new WorkerCommand(worker));command.add(new LightOffCommand(light));document.getElementById("btn").addEventListener("click", () => {command.execute();});
</script>
这种写法的优点:
- 解耦调用者与执行者 按钮点击事件(调用者)并不直接调用
light.on()
或worker.do()
,而是交给命令对象去处理。 这样使得界面逻辑和业务逻辑分离,提高了可维护性。- 易于扩展新的命令 如果需要新增功能,比如“打开风扇”或“播放音乐”,只需要定义一个新的命令类并加入命令队列即可,不需要修改已有代码。 符合 开放封闭原则
(OCP)
:对扩展开放,对修改关闭。- 支持组合命令
Command
类中的stack
可以保存多个命令,可以轻松实现宏命令(一组命令的集合),如示例中的一键执行开灯、工作、关灯等操作。 后续也可以支持撤销/重做等功能(只需记录历史栈)。- 便于测试与复用 每个命令是独立的对象,可以单独测试其
execute()
方法。 命令可以在不同上下文中复用,例如在定时器中触发、远程调用等。- 提升代码可读性和结构清晰度 将每个操作抽象为类,有助于理解意图
(Intent)
。 比如看到new LightOnCommand(light)
,就知道这是“开灯”的命令,比直接调用函数更具语义化。
总的来说:通过组合命令模式可以实现良好的职责分离,灵活扩展和统一控制,如果需求遇到了对多个操作进行封装调度记录和撤销的时候,可以使用组合命令实现。
使用命令模式实现文本编辑器
如下举例加深命令模式的使用,如下我想实现一个文本编辑器,其中功能有【清空内容
、转为大写
、转为小写
、撤销
、重做
、指令列表
】
目标
实现一个基于命令模式的文本编辑器,具备【清空内容
、转为大写
、转为小写
、撤销
、重做
、指令列表
,显示每一步操作的命令记录
】
关键类说明
Editor
(接收者):
class Editor {constructor() {this.content = "";}
}
存储当前文本内容,所用命令的实际执行者。
TextChangeCommand
(基础命令)
class TextChangeCommand {constructor(editor, newText) {this.editor = editor;this.newText = newText;this.previousText = editor.content;}execute() {this.editor.content = this.newText;}undo() {this.editor.content = this.previousText;}
}
表示每次文本输入变更的操作,记录修改前后的状态,支持撤销。
CommandManager
(扩展命令)
class CommandManager {constructor() {this.tack = [];}execute(command) {if (command) {this.tack.push(command);command.execute();updateUI();}}// 清空redo() {this.tack = [];updateUI();}// 撤销undo() {if (this.tack.length > 0) {const command = this.tack.pop();command.undo();updateUI();} else {console.log("没有可撤销的命令");updateUI();return;}}// 查看命令列表getTackList() {return this.tack;}}
使用栈(tack)
保存所有已执行命令,提供 execute()
、undo()
、redo()
、getTackList()
方法,控制整个命令流程。
UpperCaseCommand
(命令管理器)
class UpperCaseCommand {
constructor(editor) {this.editor = editor;this.previousText = editor.content;this.newText = editor.content.toUpperCase();}execute() {this.editor.content = this.newText;}undo() {this.editor.content = this.previousText;}
}
将文本转为大写的命令,同样支持撤销,可以继续扩展更多命令如 LowerCaseCommand
,ClearCommand
等。
实现的效果
交互逻辑流程
- 初始化
创建Editor
和CommandManager
,设置初始文本为空,绑定DOM
元素(如textarea
、按钮)。 - 用户操作触发命令,输入文字 → 触发
input
事件 → 创建TextChangeCommand
→ 执行并入栈
点击按钮(清空、大写、小写)→ 创建对应命令 → 执行并入栈。 - 撤销 / 重做,“撤销”点击 → 从栈中弹出最后一个命令 → 调用
.undo()
,“重做”点击 → 清空栈(当前简单实现) 当前重做只是清空栈,没有真正实现“恢复撤销”的动作,可进一步改进。
所有代码:
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>命令模式文本编辑器</title><style>body {margin: 0;padding: 0;display: flex;justify-content: center;align-items: center;height: 100vh;background: linear-gradient(45deg, #ff6b6b, #c471ad);font-family: Arial, sans-serif;}.editor-container {width: 300px;background: white;border-radius: 5px;box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);}.header {background: #2c3e50;color: white;text-align: center;padding: 10px 0;border-top-left-radius: 5px;border-top-right-radius: 5px;}.content {padding: 20px;height: 150px;border-bottom: 1px solid #ddd;}.buttons {display: flex;justify-content: space-around;padding: 10px;}.buttons button {padding: 8px 15px;border: none;border-radius: 3px;cursor: pointer;}.buttons .primary {background: #3498db;color: white;}.buttons .secondary {background: #ecf0f1;color: #333;}#contentText {border: none;height: 150px;width: 100%;}#tackListView {border: none;height: 130px;width: 100%;}</style></head><body><div class="editor-container"><div class="header">命令模式文本编辑器</div><div class="content"><!-- 文本编辑区域 --><textarea id="contentText"></textarea></div><div class="buttons"><button class="primary" id="clearBtn">清空内容</button><button class="primary" id="upperBtn">转为大写</button><button class="primary" id="lowerBtn">转为小写</button></div><div class="buttons"><button class="secondary" id="undoBtn">撤销</button><button class="secondary" id="redoBtn">重做</button><button class="secondary" id="stackList">指令列表</button></div><div class="content">命令列表:<textarea id="tackListView"></textarea></div></div><script>class TextChangeCommand {constructor(editor, newText) {this.editor = editor;this.newText = newText;this.previousText = editor.content;}execute() {this.editor.content = this.newText;}undo() {this.editor.content = this.previousText;}}class CommandManager {constructor() {this.tack = [];}execute(command) {if (command) {this.tack.push(command);command.execute();updateUI();}}// 清空redo() {this.tack = [];updateUI();}// 撤销undo() {if (this.tack.length > 0) {const command = this.tack.pop();command.undo();updateUI();} else {console.log("没有可撤销的命令");updateUI();return;}}// 查看命令列表getTackList() {return this.tack;}}class UpperCaseCommand {constructor(editor) {this.editor = editor;this.previousText = editor.content;this.newText = editor.content.toUpperCase();}execute() {this.editor.content = this.newText;}undo() {this.editor.content = this.previousText;}}// 接收者class Editor {constructor() {this.content = "";}}// 初始化const editor = new Editor();const commandManager = new CommandManager();// DOM元素const textarea = document.getElementById("contentText");// 设置初始内容editor.content = textarea.value;// 事件监听textarea.addEventListener("input", function () {const command = new TextChangeCommand(editor, textarea.value);commandManager.execute(command);});document.getElementById("clearBtn").addEventListener("click", function () {const command = new TextChangeCommand(editor, "");commandManager.execute(command);});document.getElementById("upperBtn").addEventListener("click", function () {const command = new UpperCaseCommand(editor);commandManager.execute(command);});document.getElementById("lowerBtn").addEventListener("click", function () {const command = new TextChangeCommand(editor,textarea.value.toLowerCase());commandManager.execute(command);});document.getElementById("undoBtn").addEventListener("click", function () {commandManager.undo();});document.getElementById("redoBtn").addEventListener("click", function () {const command = new TextChangeCommand(editor, "");commandManager.execute(command);commandManager.redo();});document.getElementById("stackList").addEventListener("click", function () {console.log(commandManager.getTackList());});// 更新UIfunction updateUI() {// 更新主文本区域textarea.value = editor.content;// 获取命令列表显示区域const tackListView = document.getElementById("tackListView");// 获取当前命令栈const commands = commandManager.getTackList();// 格式化命令记录let logText = "";for (let i = 0; i < commands.length; i++) {const cmd = commands[i];if (cmd instanceof TextChangeCommand) {logText += `${i + 1}. 文本修改为: ${cmd.newText}\n`;} else if (cmd instanceof UpperCaseCommand) {logText += `${i + 1}. 转为大写: ${cmd.newText}\n`;}}// 如果没有命令,显示提示信息if (commands.length === 0) {logText = "暂无命令记录";}// 更新命令列表显示区域tackListView.value = logText;}// 初始化UI更新updateUI();</script></body>
</html>
总结
设计模式不是“炫技”,而是"沉淀",希望通过阅读和学习《JavaScript设计模式》和实践中,在显示业务需求开发中写出更具有可维护性,可扩展性的代码。
致敬—— 《JavaScript设计模式》· 曾探