对于我正在开发的应用程序,我需要实现支持撤消/重做操作的命令模式。我还想使用 javascript ES6 新功能,如类、常量等。 我从来没有使用过这种模式因此我真的很想知道使用 javascript es6 语法可以很好地实现这种模式。
在 Google 上阅读此模式后,我使用一个小示例编写了自己的实现,其中只有 2 种类型的命令(更新和删除)和一个简单的模型数据库。
欢迎大家批评指正!
模型DB.js
// mockupDB.js
export default {
key1: "value1",
key2: "value2",
key3: "value3"
}
命令.js
// Command.js
import mockupDB from "./mockupDB";
class Command {
constructor(execute, undo, serialize, value) {
this.execute = execute;
this.undo = undo;
this.serialize = serialize;
this.value = value;
}
}
export function UpdateCommand(key, value) {
let oldValue;
const execute = () => {
if (mockupDB.hasOwnProperty(key)) {
oldValue = mockupDB[key];
mockupDB[key] = value;
}
};
const undo = () => {
if (oldValue) {
mockupDB[key] = oldValue;
}
};
const serialize = () => {
return JSON.stringify({type: "Command", action: "update", key: key, value: value});
};
return new Command(execute, undo, serialize, value);
}
export function DeleteCommand(key) {
let oldValue;
const execute = () => {
if (mockupDB.hasOwnProperty(key)) {
oldValue = mockupDB[key];
delete mockupDB[key];
}
};
const undo = () => {
mockupDB[key] = oldValue;
};
const serialize = () => {
return JSON.stringify({type: "Command", action: "delete", key: key});
};
return new Command(execute, undo, serialize);
}
CommandManager.js
// CommandManager.js
export class CommandManager {
constructor() {
this.executeHistory = [];
this.undoHistory = [];
}
execute(command) {
this.executeHistory.push(command);
command.execute();
console.log(`Executed command ${command.serialize()}`);
}
undo() {
let command = this.executeHistory.pop();
if (command) {
this.undoHistory.push(command);
command.undo();
console.log(`Undo command ${command.serialize()}`)
}
}
redo() {
let command = this.undoHistory.pop();
if (command) {
this.executeHistory.push(command);
command.execute();
console.log(`Redo command ${command.serialize()}`);
}
}
}
client.js
// client.js
import {CommandManager} from "./CommandManager";
import {UpdateCommand, DeleteCommand} from "./Command";
import mockupDB from "./mockupDB";
"use strict";
const main = () => {
const commandManager = new CommandManager();
console.log("DB status", mockupDB, "\n");
commandManager.execute(new UpdateCommand("key2", "newValue2"));
commandManager.execute(new DeleteCommand("key3"));
console.log("DB status", mockupDB, "\n");
commandManager.undo();
commandManager.undo();
console.log("DB status", mockupDB, "\n");
commandManager.redo();
commandManager.redo();
console.log("DB status", mockupDB, "\n");
};
main();
输出
DB status { key1: 'value1', key2: 'value2', key3: 'value3' }
Executed command {"type":"Command","action":"update","key":"key2","value":"newValue2"}
Executed command {"type":"Command","action":"delete","key":"key3"}
DB status { key1: 'value1', key2: 'newValue2' }
Undo command {"type":"Command","action":"delete","key":"key3"}
Undo command {"type":"Command","action":"update","key":"key2","value":"newValue2"}
DB status { key1: 'value1', key2: 'value2', key3: 'value3' }
Redo command {"type":"Command","action":"update","key":"key2","value":"newValue2"}
Redo command {"type":"Command","action":"delete","key":"key3"}
DB status { key1: 'value1', key2: 'newValue2' }
这是一个基于意见的问题。在我看来:
OOP 增加了代码量,对 JS 来说很麻烦,所以我不会对命令设计模式的每个角色(调用者、命令、接收者)都使用
class
。一个带有闭包的 JS 函数来表示命令对象就足够了
您的实施中缺少一个角色 - receiver。您可以将满足接口的接收者对象传递给命令对象,而不是直接在命令对象中使用具体的接收者对象。这称为依赖倒置原则。命令对象和接收者对象应该依赖于抽象(接收者接口 - TypeScript 中的
interface
)而不是具体(接收者具体 - 类的实例)
从命令模式的wiki,我们知道:
命令对象知道接收器并调用接收器的方法。接收方方法的参数值存储在命令中。
接收者对象拥有由命令的执行方法调用的方法。
redoStack
和undoStack
可能会更好。我的实施将是:
const updateCommand = (receiver, data) => {
let oldValue;
const execute = () => {
oldValue = receiver.get(data.key);
receiver.update(data.key, data.value);
}
const undo = () => {
receiver.update(data.key, oldValue);
}
const toJSON = () => {
return { type: "Command", action: "update", data }
}
return { execute, undo, toJSON }
}
const deleteCommand = (receiver, data) => {
let oldValue;
const execute = () => {
oldValue = receiver.get(data.key);
receiver.delete(data.key)
}
const undo = () => {
receiver.create(data.key, oldValue);
}
const toJSON = () => {
return { type: "Command", action: "delete", data }
}
return { execute, undo, toJSON }
}
const commandManager = {
redoStack: [],
undoStack: [],
execute(command) {
this.redoStack.push(command);
command.execute();
console.log(JSON.stringify(command));
},
undo() {
const command = this.redoStack.pop();
if (command) {
this.undoStack.push(command);
command.undo();
console.log(JSON.stringify(command));
}
},
redo() {
const command = this.undoStack.pop();
if (command) {
this.redoStack.push(command);
command.execute();
console.log(JSON.stringify(command));
}
}
}
const fakeDB = {
data: {
key1: "value1",
key2: "value2",
key3: "value3"
},
get(key) {
return this.data[key];
},
update(key, value) {
this.data[key] = value;
},
delete(key) {
delete this.data[key];
},
create(key, value) {
this.data[key] = value;
},
}
// other receivers with same interface
const fakeMongoDB = {}
function main() {
console.log("DB status", fakeDB.data, "\n");
commandManager.execute(updateCommand(fakeDB, { key: 'key2', value: 'newValue2' }));
commandManager.execute(deleteCommand(fakeDB, { key: 'key3' }));
console.log("DB status", fakeDB.data, "\n");
commandManager.undo();
commandManager.undo();
console.log("DB status", fakeDB.data, "\n");
commandManager.redo();
commandManager.redo();
console.log("DB status", fakeDB.data, "\n");
}
main();