命令模式
有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么,此时希望用一种松耦合的方式来设计软件,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。
- MenuBar 实现具体的操作
- RefreshMenuBarCommand 执行这个操作,但是不知道操作是什么,接收者是谁
- setCommand 为DOM对象绑定对应的指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const button = document.getElementById('button');
const setCommand = function (button, command) { button.onclick = function () { command.execute(); } }; const MenuBar = { refresh: function () { console.log('刷新菜单界面'); } }; const RefreshMenuBarCommand = function (receiver) { return { execute: () => { receiver.refresh(); } } };
const rfCommand = RefreshMenuBarCommand(MenuBar);
setCommand(button, rfCommand);
|
撤销&重做
保存上一次做的内容,当调用undo时返回到上一次的状态。
- 此时我们需要维护一个堆栈,因为我们可能希望撤销到很多步之前。
- 然后通过指针移动进行撤销操作,正常情况下指针指向当前的操作,撤销时指针回退。
- 既然有了指针和堆栈,那么我们也可以顺带实现重做(redo),只不过是将指针前移改成后移。
1 2 3 4
| <button id="undo">undo</button> <button id="update">update</button> <input type="text" id="input"> <span id="text"></span>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| const undo = document.getElementById('undo'); const redo = document.getElementById('redo'); const update = document.getElementById('update'); const input = document.getElementById('input'); const text = document.getElementById('text');
class InputCommand { constructor(receiver, dom) { this.receiver = receiver this.dom = dom this.value = null this.cache = [] this.current = this.cache.length - 1 } execute() { this.value = this.dom.value this.cache.push(this.value) this.current++ this.receiver(this.value) } undo() { this.receiver(this.cache[--this.current]) } redo() { this.receiver(this.cache[++this.current]) } }
const inputCommand = new InputCommand((value) => { text.innerText = value }, input)
update.onclick = () => { inputCommand.execute() }
undo.onclick = () => { inputCommand.undo() }
redo.onclick = () => { inputCommand.redo() }
|
其他
一般来讲,我们会根据单一职责原则,每个命令负责一个原子化的操作,然后将这些命令组合起来。
- 唱(sing)
- 跳(sang)
- Rap(rap)
- 篮球(ball)
同时我们不希望上一个命令还没有执行完的时候就执行下一个命令,我们就需要回调函数或者发布订阅的形式来保证命令的有序执行。
此外我们可能会每次执行一组命令,之后回退也希望回退的单位是按照组划分,这样的形式被我们成为 宏命令 。例如键盘或者鼠标的 宏定义 ,按下某个按键执行一组动作,就是宏命令的一种体现。
延申—组合模式
上面提到了宏命令,可以执行一组子命令,而在组合模式中,也可以实现相同的操作。
组合模式
组合模式是对象组合成树形结构,以表示“部分-整体”的层次结构。 除了用来表示树形结构之外,组合模式的另一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使 用具有一致性。
- 表示树形结构。组合模式提供了一种遍历树形结构的方案,通过调用组合对象的execute方法,程序会递归调用组合对象下面的叶对象的execute方法,所以我们只需要一次操作就可以完成多件事情。组合模式可以非常方便地描述对象部分-整体层次结构。
- 利用对象多态性统一对待组合对象和单个对象。利用对象的多态性表现,可以使客户端忽略组合对象和单个对象的不同。在组合模式中,客户将统一地使用组合结构中的所有对象,而不需要关心它究竟是组合对象还是单个对象。
简单实现
由于Javascript类型判断是通过鸭子类型的方式,因此我们只需要保证调用的接口一致(均为execute),而在Java这种结构化类型语言中,需要实现一个抽象类,根节点和叶子节点的类都继承这个抽象类来实现组合模式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| class Father { constructor() { this.executeList = []; } execute() { this.executeList.forEach((item) => { item.execute(); }); } add(execute) { this.executeList.push(execute); } }
class Child { constructor(value) { this.value = value; } execute() { console.log(this.value); } add() { throw Error("普通对象不允许添加叶子节点"); } }
const father1 = new Father(); const child1_1 = new Child("1-1"); const child1_2 = new Child("1-2"); const child1_3 = new Child("1-3"); father1.add(child1_1); father1.add(child1_2); father1.add(child1_3); const father2 = new Father(); const child2_1 = new Child("2-1"); const child2_2 = new Child("2-2"); const child2_3 = new Child("2-3"); father2.add(child2_1); father2.add(child2_2); father2.add(child2_3);
father1.add(father2);
father1.execute();
|
此时father1就是一个树型结构,当调用father1的execute方法,会依次调用子节点的execute方法,这样可以很方便的遍历整棵树。

同时新增和删除节点也会变得很方便,例如为father2添加一个子节点child2_4,为father1删除子节点child1_1。
只需要:
1 2 3 4 5 6 7 8 9 10 11
| const child2_4 = new Child("2-4"); father2.add(child2_4); father1.remove(child1_1);
father1.execute();
remove(execute) { const index = this.executeList.indexOf(execute); this.executeList.splice(index, 1); }
|
使用场景
组合模式可以方便地构造一棵树来表示对象的部分—整体结构。特别是我们在开发期间不确定这棵树到底存在多少层次的时候。在树的构造最终完成之后,只需要通过请求树的最顶层对象,便能对整棵树做统一的操作。在组合模式中增加和删除树的节点非常方便,并且符合开放—封闭原则。
组合模式使客户可以忽略组合对象和叶对象的区别,客户在面对这棵树的时候,不用关心当前正在处理的对象是组合对象还是叶对象,也就不用写一堆 if、else 语句来分别处理它们。组合对象和叶对象会各自做自己正确的事情,这是组合模式最重要的能力。
缺点
- 它可能会产生一个这样的系统:系统中的每个对象看起来都与其他对象差不多。它们的区别只有在运行的时候会才会显现出来,这会使代码难以理解。
- 如果通过组合模式创建了太多的对象,那么这些对象可能会让系统负担不起。