我们知道地球和一些其他行星围绕着太阳旋转,也知道在一个原子中,有许多电子围绕着原子核旋转。可以大胆推测,一个电子中也包含了另一个宇宙,我们所熟知的银河系

也只是另一个浩大空间的组成部分.....

在程序设计中,组合模式就是用小的对象来构建更大的对象。

先来看一个简单的例子:

1 var closeDoorCommand = {
2 excute: function() {
3 console.log('关门');
4 }
5 };
6
7 var openPcCommand = {
8 excute: function() {
9 console.log('开电脑');
10 }
11 };
12
13 var openQQCommand = {
14 excute: function() {
15 console.log('登录QQ');
16 }
17 };
18
19 var MacroCommand = function() {
20 return {
21 commandList: [],
22 add: function(command) {
23 this.commandList.push(command);
24 },
25 excute: function() {
26 for(var i = 0, command; command = this.commandList[ i++ ]; ){
27 command.excute();
28 }
29 }
30 }
31 };
32
33 var macroCommand = new MacroCommand();
34 macroCommand.add( closeDoorCommand );
35 macroCommand.add( openPcCommand );
36 macroCommand.add( openQQCommand );
37

1.首先我们定义了一系列小的命令对象,他们有关门、打开电脑、登录QQ等一系列具有顺序的操作,每个命令都具有一个可供执行的execute方法

2.定义了一个可以包含其他命令的宏命令,这个宏命令包含了一个等待执行的命令队列及启动命令的execute方法。

 

在宏命令中,保存了对每个子对象的引用,并且实现了和子对象相同的方法:execute方法,只要调用宏命令的execute方法,程序就会依次执行commandList中的所有命令。

 

组合模式的用途:

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

的使用具有一致性。

  • 表示树形结构。通过回顾上面的例子,可以发现组合模式的一个有点是:提供了一种遍历树形结构的方案,通过调用组合对象的execute方法,程序会递归调用组合对象的线面叶对象的
    execute方法。
  • 利用多态性统一对待组合对象和单个对象。利用对象的多态性表现,可以使客户端忽略组合对象和单个对象的不同。在组合模式中,客户统一地使用组合结构中的所有对象,而不需要关心它究竟是组合对象还是单个对象。

这些在实际开发中会给用户带来相当大的便利性。我们只需要确定它是一个命令,并且这个命令拥有可执行的execute方法,那么这个命令就可以被添加的宏命令中。

 

其实,利用组合模式可以实现更强大的宏命令。

1 <!DOCTYPE html>
2 <html lang="en">
3 <head>
4 <meta charset="UTF-8">
5 <title>组合模式</title>
6 </head>
7 <body>
8 <div>
9 <button id="button">万能遥控</button>
10 </div>
11 <script>
12
13 var MacroCommand = function() {
14 return {
15 commandList: [],
16 add: function(command) {
17 this.commandList.push(command);
18 },
19 execute: function() {
20 for(var i = 0, command; command = this.commandList[ i++ ]; ){
21 command.execute();
22 }
23 }
24 }
25 };
26
27 var openAcCommand = {
28 execute: function() {
29 console.log('打开空调');
30 }
31 };
32
33 /*家里的电视和音响是连在一起的,所以可以用一个宏命令来组合打开电视和打开音响的命令*/
34
35 var openTVCommand = {
36 execute: function() {
37 console.log('打开电视');
38 }
39 };
40
41 var openSoundCommand = {
42 execute: function() {
43 console.log('打开音响');
44 }
45 };
46
47 var macroCommand1 = MacroCommand();
48 macroCommand1.add( openTVCommand );
49 macroCommand1.add( openSoundCommand );
50
51
52 /*关门、打开电脑和打开登录QQ的命令*/
53 var closeDoorCommand = {
54 execute: function() {
55 console.log('关门');
56 }
57 };
58
59 var openPcCommand = {
60 execute: function() {
61 console.log('开电脑');
62 }
63 };
64
65 var openQQCommand = {
66 execute: function() {
67 console.log('登录QQ');
68 }
69 };
70
71
72 var macroCommand2 = MacroCommand();
73 macroCommand2.add( closeDoorCommand );
74 macroCommand2.add( openPcCommand );
75 macroCommand2.add( openQQCommand );
76
77 /*把所有的命令组合成一个“超级命令”*/
78
79 var macroCommand = MacroCommand();
80 macroCommand.add( openAcCommand );
81 macroCommand.add( macroCommand1 );
82 macroCommand.add( macroCommand2 );
83
84 /*事件绑定*/
85 var setCommand = (function(command) {
86 document.getElementById('button').onclick = function() {
87 command.execute();
88 }
89 })(macroCommand)
90 </script>
91 </body>
92

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

前面说过,组合模式最大的优点在于可以一致性的对待组合对象和基本对象。客户不需要知道当前处理的是宏命令还是普通命令,只要它是一条命令,并且拥有execute方法,这个命令就可以

被添加到书中

这种透明性带来的便利,在静态语言中体现的尤为明显。比如在Java中,实现组合模式的关键是Composite类和Leaf类都必须继承自一个Component抽象类。

代码如下:

1 // Java代码
2 public abstract class Component{
3 public void add( Component child) {}
4 public void remove( Component child){}
5 }
6
7 public class Composite extends Component{
8 public void add( Component child) {}
9 public void remove( Component child) {}
10 }
11
12 public class Leaf extends Component{
13 public void add( Component child){
14 throw new UnsupportedOperationException()
15 }
16 public void remove( Component child){}
17
18 }
19
20 public class client{
21 public static void main( String args[]) {
22 Component root = new Composite();
23 Component c1 = new Composite();
24 Component c2 = new Composite();
25
26 Component leaf1 = new Leaf();
27 Component leaf2 = new Leaf();
28
29 root.add(c1);
30 root.add(c2);
31
32 c1.add(leaf1);
33 c1.add(leaf2);
34
35 root.remove();
36 }
37

在JavaScript这种动态语言中,对象的多态性是与生俱来的,也没有编译器去检查变量的类型,在JavaScript中实现组合模式的难点在于要保证组合对象和叶对象拥有同样的方法,这通常需要鸭子类型的思想对他们进行接口检查。

 

在JavaScript中实现组合模式,看起来缺乏一些严谨性,代码算不上安全,但能更快和自由的开发,这既是JavaScript的缺点,也是优点。

 

一些值得注意的地方:

  1. 组合模式不是父子关系
    组合模式是一种HAS-A(聚合)的关系,而不是IS-A。组合对象吧请求委托给它所包含的所有叶对象,它们能够合作的关键是拥有相同的接口。
  2. 对叶对象操作的一致性
    组合模式除了要求组合对象和叶对象拥有相同的接口外,还有一个必要条件,就是一组叶对象的操作必须具有一致性。
    比如:公司给全体员工发元旦过节费,这个场景刻印用组合模式,但如果公司给今天过生日的员工发送一封生日祝福的邮件,组合模式就没有用武之地了,除非先把
    今天过生日的员工挑选出来。只有用一致的方式对待列表中的每个叶对象,才适合使用组合模式。
  3. 双向映射关系发放过节费的通知步骤是从公司到各个部门,再到各个小组,最后到每个员工的邮箱里。这本身是个组合模式的好例子,但是要考虑的一种情况是,也许某个员工属于多个组织
    架构,对象之间的关系并不是严格意义上的层次结构,在这种情况下,是不适合用组合模式的,部分员工可能会收到两份过节费。
    这种情况下必须给父节点和子节点建立双向映射关系,一个简单的方法是给小组和员工对象都增加集合来保存对方的引用。但是这种相互间的引用相当复杂,而且对象间产生了
    过多的耦合性,修改或者删除一个对象都变得困难,此时可以考虑用中介者模式来管理这些对象。
  4. 利用职责链来提高组合性能
    在组合模式中,如果树的结构比较复杂,节点数量很多,在遍历树的过程中,性能方面也许表现的不够理想。这时需要借助一些技巧,比如借助职责链模式。
    职责链模式一般需要手动设置链条,但在组合模式中,父对象和子对象之间形成了天然的职责链。让请求顺着链条从父对象往子对象传递,或者是反过来从子对象
    往父对象传递,直到遇到可以处理该请求的对象为止,这也是职责链模式的经典运用场景之一。

 

最后,说一下组合模式中引用父对象的例子。

1 var Folder = function(name) {
2 this.name = name;
3 this.parent = null;
4 this.files = [];
5 }
6 Folder.prototype.add = function(file) {
7 file.parent = this;
8 this.files.push(file);
9 }
10 Folder.prototype.scan = function() {
11 console.log('开始扫描文件夹:' + this.name);
12 for (var i = 0, file, files = this.files; file = files[ i++ ]; ) {
13 file.scan();
14 }
15 }
16 Folder.prototype.remove = function() {
17 if (!this.parent) { // 根节点或者树外的游离节点
18 return;
19 }
20 for (var files = this.parent.files, l = files.length - 1; l >= 0; l--) {
21 var file = files[l];
22 if (file === this) {
23 fils.splice(l, 1);
24 }
25 }
26 }
27
28 var File = function(name) {
29 this.name = name;
30 this.parent = null;
31 }
32
33 File.prototype.add = function() {
34 throw new Error('不能添加在文件下面');
35 }
36
37 File.prototype.scan = function() {
38 console.log('开始扫描文件:' + this.name);
39 }
40
41 File.prototype.remove = function() {
42 if (!this.parent) {
43 return;
44 }
45 for (var files = this.parent.files, l = files.length - 1; l >= 0; l--) {
46 if (file === this ) {
47 files.splice(l ,1);
48 }
49 }
50

总结:何时使用组合模式

  组合模式如果运用得当,可以大大简化客户的代码。一般来说,组合模式适用于以下这两种情况:

  1. 表示对象的部分整体层次结构。组合模式可以方便地构造一棵树来表示对象的部分整体结构。特别是在开发期间不确定这棵树会存在多少层次的时候。在树的构造最终完成之后,只需要请求
    树的顶层对象,便能对树做统一的操作。
  2. 客户希望统一对待树中的所有对象。组合模式使客户可以忽略组合对象和叶对象的区别,客户在面对这棵树的时候,不用关心当前处理的对象是组合对象还是叶对象,也就是不用写一堆if,else语句来处理它们。组合对象和叶对象会各自做自己正确的事,这是组合模式最重要的能力。