JavaScript设计模式与开发实践(六)命令模式&组合模式

命令模式

有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么,此时希望用一种松耦合的方式来设计软件,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。

  1. MenuBar 实现具体的操作
  2. RefreshMenuBarCommand 执行这个操作,但是不知道操作是什么,接收者是谁
  3. 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时返回到上一次的状态。

  1. 此时我们需要维护一个堆栈,因为我们可能希望撤销到很多步之前。
  2. 然后通过指针移动进行撤销操作,正常情况下指针指向当前的操作,撤销时指针回退。
  3. 既然有了指针和堆栈,那么我们也可以顺带实现重做(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()
}

其他

一般来讲,我们会根据单一职责原则,每个命令负责一个原子化的操作,然后将这些命令组合起来。

  1. 唱(sing)
  2. 跳(sang)
  3. Rap(rap)
  4. 篮球(ball)

同时我们不希望上一个命令还没有执行完的时候就执行下一个命令,我们就需要回调函数或者发布订阅的形式来保证命令的有序执行。

此外我们可能会每次执行一组命令,之后回退也希望回退的单位是按照组划分,这样的形式被我们成为 宏命令 。例如键盘或者鼠标的 宏定义 ,按下某个按键执行一组动作,就是宏命令的一种体现。

延申—组合模式

上面提到了宏命令,可以执行一组子命令,而在组合模式中,也可以实现相同的操作。

组合模式

组合模式是对象组合成树形结构,以表示“部分-整体”的层次结构。 除了用来表示树形结构之外,组合模式的另一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使 用具有一致性。

  1. 表示树形结构。组合模式提供了一种遍历树形结构的方案,通过调用组合对象的execute方法,程序会递归调用组合对象下面的叶对象的execute方法,所以我们只需要一次操作就可以完成多件事情。组合模式可以非常方便地描述对象部分-整体层次结构。
  2. 利用对象多态性统一对待组合对象和单个对象。利用对象的多态性表现,可以使客户端忽略组合对象和单个对象的不同。在组合模式中,客户将统一地使用组合结构中的所有对象,而不需要关心它究竟是组合对象还是单个对象。

简单实现

由于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(); // 1-1 1-2 1-3 2-1 2-2 2-3

此时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(); // 1-2 1-3 2-1 2-2 2-3 2-4

// Father类新增remove方法
remove(execute) {
const index = this.executeList.indexOf(execute);
this.executeList.splice(index, 1);
}

使用场景

  • 表示对象的部分—整体层次结构。

组合模式可以方便地构造一棵树来表示对象的部分—整体结构。特别是我们在开发期间不确定这棵树到底存在多少层次的时候。在树的构造最终完成之后,只需要通过请求树的最顶层对象,便能对整棵树做统一的操作。在组合模式中增加和删除树的节点非常方便,并且符合开放—封闭原则。

  • 客户希望统一对待树中的所有对象。

组合模式使客户可以忽略组合对象和叶对象的区别,客户在面对这棵树的时候,不用关心当前正在处理的对象是组合对象还是叶对象,也就不用写一堆 if、else 语句来分别处理它们。组合对象和叶对象会各自做自己正确的事情,这是组合模式最重要的能力。

缺点

  • 它可能会产生一个这样的系统:系统中的每个对象看起来都与其他对象差不多。它们的区别只有在运行的时候会才会显现出来,这会使代码难以理解。
  • 如果通过组合模式创建了太多的对象,那么这些对象可能会让系统负担不起。

JavaScript设计模式与开发实践(六)命令模式&组合模式
https://jing-jiu.github.io/jing-jiu/2023/01/10/notebooks/JavaScript设计模式与实践/设计模式(六)/
作者
Jing-Jiu
发布于
2023年1月10日
许可协议