JavaScript设计模式笔记-组合模式

10.1 回顾宏命令

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
var closeDoorCommand={
execute:function(){
console.log("关门")
}
}
var openPcCommand={
execute=function(){
console.log("开电脑")
}
}
var openQQCommand={
execute:function(){
console.log("登陆QQ")
}
}
var MacroCommand=function(){
return{
commandsList:[],
add:function(){
this.commandsList.push(command)
},
execute:function(){
for(var i=0,command;command=this.commandsList[i++]){
command.execute()
}
}
}
}
var macroCommand = MacroCommand();
macroCommand.add( closeDoorCommand );
macroCommand.add( openPcCommand );
macroCommand.add( openQQCommand );
macroCommand.execute();

宏命令对象包含了一组具体的子命令对象,不管是宏命令对象,还是子命令对象都有一个execute方法负责执行命令.
通过观察这个宏命令可以发现这是一颗很简单的树.
其中marcoCommand被称为组合对象,closeDoorCommand,openPcCommand,openQQCommand都是叶对象.在macroCommand的execute方法中,并不执行真正的操作,而是遍历它所包含的叶对象,把真正的execute请求1委托给这些叶对象.
MarcoCommand实际上只是一组命令的代理,并非真正的代理.macroCommand只负责传递请求给叶对象,它的目的不在于控制对叶对象的访问.

10.2 组合模式的用途

组合模式将对象组合成树形结构,以表示“部分-整体”的层次结构.

  • 表示树形结构.提供了一种遍历树形结构的方案,通过调用组合对象的execute方法,程序就会递归调用组合对象下面的叶对象的execute方法,只需要执行一次execute操作即可.
  • 利用对象多态性统一对待组合对象和单个对象.利用对象的多态性表现,可以使客户端忽略组合对象和单个对象的不同.

当我们往“万能遥控器”添加一个命令的时候,并不需要关心这个命令是宏命令还是普通命令.我们只需要确定它是一个命令,并且这个命令拥有可执行的execute方法那么这个命令就可以被添加进万能遥控器.

10.3 请求在树中传递个过程

图略.请购买正版图书 相信我这本书绝对物有所值.
以宏命令为例,请求从树最顶端往下传递,如果当前处理请求的对象是叶对象(普通子命令),叶对象自身会对请求作出相应的处理;如果当前处理对象是组合对象(宏命令),则组合对象会遍历它下属子节点,将请求继续传递给这些子节点.
组合对象嵌套组合对象 的这种形式

10.4 更为强大的宏命令

比如我们要实现一个遥控器,有以下功能

  • 打开空调
  • 打开电视和音响
  • 关门,开电脑,登陆QQ
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<body>
<button id='button'>click me</button>
</body>
<script type="text/javascript">
var MacroCommand=function(){
var command_list=[];
var add=function(command){
command_list.push(command)
}
var execute=function(){
command_list.forEach(function(e,i){
e.execute()
})
}
return{
add:add,
execute:execute
}
}
var openAcCommand={
execute:function(){
console.log("打开空调")
}
}
//打开电视和音响
var openTvCommand={
execute:function(){
console.log("打开电视")
}
}
var openSoundCommand={
execute:function(){
console.log("打开音响")
}
}
var macroCommand1=MacroCommand();
macroCommand1.add(openTvCommand);
macroCommand1.add(openSoundCommand);
//关门,开电脑,登陆QQ
var closeDoorCommand = {
execute: function(){
console.log( '关门' );
}
};
var openPcCommand = {
execute: function(){
console.log( '开电脑' );
}
};
var openQQCommand = {
execute: function(){
console.log( '登录QQ' );
}
};
var macroCommand2 = MacroCommand();
macroCommand2.add( closeDoorCommand );
macroCommand2.add( openPcCommand );
macroCommand2.add( openQQCommand );
//组合成超级宏命令
var superMacroCommand=MacroCommand();
superMacroCommand.add(macroCommand1);
superMacroCommand.add(macroCommand2);
//绑定按钮
document.getElementById("button").onclick=function(){
superMacroCommand.execute()
}
</script>

这个例子中,基本对象被组合成了更为复杂的组合对象,组合对象又可以被组合.最终只需要调用上层的execute方法即可.每当最上层的对象进行一次请求时,实际上是在对整个树进行深度优先的搜索.而创建组合对象的程序猿并不关心这些内在的细节.往这棵树里面添加一些新的节点对象是非常容易的事.

10.5 抽象类在组合模式中的作用

java跪了.这块说的是java实现组合模式.这里不做笔记(实际上是看不懂 笑 哈哈哈哈哈哈)
因为在JavaScript中,对象的多态性是与生俱来的,所以不会用JavaScript模拟一个怪异的抽象类.虽然少了一些严谨性.

10.6 透明性带来的安全问题

组合模式的透明性使得发起请求的客户不用去顾忌树中组合对象和叶对象的区别,但它们在本质上是有区别的.
组合对象可以拥有子节点,叶对象下面就没有子节点.比如试图往叶对象下面添加子节点..这样就行不通.通常我们会给这个问题抛出一个异常来提醒客户.

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
var MacroCommand = function(){
return {
commandsList: [],
add: function( command ){
this.commandsList.push( command );
},
execute: function(){
for ( var i = 0, command; command = this.commandsList[ i++ ]; ){
command.execute();
}
}
}
};
var openTvCommand = {
execute: function(){
console.log( '打开电视' );
},
add: function(){
throw new Error( '叶对象不能添加子节点' );
}
};
var macroCommand = MacroCommand();
macroCommand.add( openTvCommand );
openTvCommand.add( macroCommand ) // Uncaught Error: 叶对象不能添加子节点

实际上就给叶对象添加一个add方法 然后这个方法会抛出异常.

10.7 组合模式的例子——扫描文件夹

文件夹和文件之间的关系,非常适合用组合模式来描述,文件夹里面既可以包含文件又可以包含其他文件夹,最终可能组合成一棵树(DOM不就是这样么),

  • 例如想把移动硬盘的学习资料移动到F盘的学习资料文件夹,在复制这些电子书的时候,我并不需要考虑这批文件的类型,不管他们是单独的电子书还是被放在了文件夹中.组合模式让Ctrl+V,Ctrl+成为了一个统一的操作.
  • 当我用杀毒软件扫描该文件夹的时候,往往不会关心里面有多少文件和子文件夹,组合模式使得我们只需要操作最外层的文件夹进行扫描.

首先定义好Folder和File这两个类

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
// Folder类
var Folder=function(name){
var name=name
var files=[];
var add=function(){
files.add(file)
}
var scan=function(){
console.log("开始扫描文件夹"+name)
files.forEach(function(e,i){
e.scan();//嗅探文件
})
}
return{
add:add,
scan:scan
}
}
// File类
var File=function(name){
var name=name;
var scan=function(){
console.log("当前文件名为"+name)
}
var add=function(){
throw new Error("当前是文件无法再继续添加")
}
return{
scan:scan,
add:add
}
}

接下来创建一些文件夹和文件对象,并且让它们组合成一棵树,这棵树就是我们F盘里的现有文件夹目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var folder=Folder("学习资料")
var folder1=Folder("JavaScript")
var folder2=Folder("jQuery")
var file1=File("JavaScript设计模式与开发实践")
var file2=File("精通jquery")
var file3=File("重构与模式")
folder1.add(file1);
folder2.add(file2);
folder.add(folder1);
folder.add(folder2);
folder.add(file3);

现在的需求是把移动硬盘里的文件和文件夹都复制到这棵树中,假设我们已经得到了这些文件对象

1
2
3
4
5
var folder3=Folder("Nodejs");
var file4=File("深入浅出Node.js");
folder3.add(file);
var file5=File("JavaScript语言精髓与编程实践");

接下来就是把这些文件都添加到原有的树中

1
2
folder.add(folder3);
folder.add(file5);

在添加一批文件的操作过程中,客户不用分辨它们到底是文件还是文件夹,新增加的文件和文件夹能够很容易的添加到原来的树结构中,和树里已有的对象一起工作.
运用到组合模式之后,扫描整个文件夹的操作也是轻而易举的,我们只需要操作树的最顶端对象:
folder.scan();

10.8 一些值得注意的地方

1.组合模式不是父子关系

组合模式的树形结构容易让人误以为组合对象和叶对象是父子关系.

2.对叶对象操作的一致性

组合模式除了组合对象和叶对象拥有相同的接口之外,还有一个必要条件是对一组叶对象的操作必须具有一致性.
比如公司要给全体员工发放元旦过节费1000块,这个场景就可以运用组合模式,但如果公司要给今天过生日的员工发送一封生日祝福邮件,除非把这些生日员工挑选出来,否则组合模式就没有用武之地了.只有用一致的方式对待列表中的每个叶对象的时候才适合使用组合模式

3.双向映射关系

发放过节费的通知步骤是从公司到各个部门,再到各个小组,最后到每个员工的邮箱里,比如某个人即属于开发组又属于架构组,那么这个人很可能会收到两份过节费。
这种复合的情况我们必须给父节点和子节点建立双向映射的关系,一个简单的方法是给小组和员工对象都增加集合来保存对方的引用.但是这种集合引用相当复杂,而且对象之间产生了过多的耦合性,此时我们可以引用中介者模式来管理

4.用职责链模式提高组合模式的性能

比如原型继承就是一种职责链模式. 这里作者没详细展开讲.

10.9 引用父对象

比如我们需要添加一个删除文件的功能,实际上是从这个文件夹所在的上层文件夹中删除文件的.现在来改写扫描文件夹的代码.
如果用闭包来实现的话 需要手动写一个get,set方法…..要不然你是无法获取到闭包内部的内容的,但是面向对象的写法不同 通过this就可以获取了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var Folder=function(name){
this.name=name;
this.parent=null;//增加this.parent属性
this.files=[];
}
Folder.prototype.add=function(file){
file.parent=this;//设置文件或者文件夹的父对象
this.files.push(file)//添加文件或者文件夹
}
Folder.prototype.scan=function(){
console.log("正在扫描文件夹:"+this.name)
this.files.forEach(function(e,i){
e.scan();
})
}

接下来增加Folder.prototype.remove方法

1
2
3
4
5
6
7
8
9
10
11
Folder.prototype.remove=function(){
if(!this.parent){//文件夹为根节点或者树外游离的节点
return
}
this.parent.files.forEach(function(e,i){//从上层文件夹删除该文件夹
if(e===this){//如果上层文件夹找到了该文件夹则删除
this.parent.files.splice(i,1)
}
})
}

File类实现基本上一致

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
var File = function( name ){
this.name = name;
this.parent = null;
};
File.prototype.add = function(){
throw new Error( '不能添加在文件下面' );
};
File.prototype.scan = function(){
console.log( '开始扫描文件: ' + this.name );
};
File.prototype.remove = function(){
if ( !this.parent ){ //根节点或者树外的游离节点
return;
}
for ( var files = this.parent.files, l = files.length - 1; l >=0; l-- ){
var file = files[ l ];
if ( file === this ){
files.splice( l, 1 );
}
}
};
var folder = new Folder( '学习资料' );
var folder1 = new Folder( 'JavaScript' );
var file1 = new Folder ( '深入浅出Node.js' );
folder1.add( new File( 'JavaScript 设计模式与开发实践' ) );
folder.add( folder1 );
folder.add( file1 );
folder1.remove(); //移除文件夹
folder.scan();

在folder1.remove()的时候实际上是从folder1的父文件夹也就是folder.files中移除这个文件夹的 那么文件原理也是一样的
通过this来判断删除文件/文件夹的

10.10 何时使用组合模式

  • 表示对象的部分-整体层次结构,组合模式可以方便的构造一棵树来表示对象的部分-整体结构,特别是我们在开发期间不确定这棵树到底存在多少层次的时候,在树构造最终完成之后只需要通过请求树的最顶层对象,便能对整个树做统一的操作.
  • 客户希望统一对待树中的所有对象,组合模式使客户忽略组合对象和叶对象的区别,客户在面对这堆树的时候,不需要关心当前正在处理的是组合对象还是叶对象.组合对象和叶对象会各自做自己正确的事情.