设计模式之组合模式

组合模式

组合模式,将对象组合成树形结构以表示“部分-整体”的层次结构,组合模式使得用户对单个对象和组合对象的使用具有一致性。掌握组合模式的重点是要理解清楚 “部分/整体” 还有 ”单个对象“ 与 “组合对象” 的含义。
组合模式可以让客户端像修改配置文件一样简单的完成本来需要流程控制语句来完成的功能。
经典案例:系统目录结构,网站导航结构等。

模式作用:

  1. 你想表示对象的部分-整体层次结构时
  2. 你希望用户忽略组合对象和单个对象的不同,用户将统一地使用组合结构中的所有对象(方法)

注意事项:

  1. 该模式经常和装饰者模式一起使用,它们通常有一个公共的父类(也就是原型),因此装饰必须支持具有add,remove,getChild操作的component接口

例子:

文件夹和文件之间的关系,非常适合用组合模式来描述.文件夹里既可以包含文件,又可以包含其他文件夹,最终可能组合成一棵树,组合模式在文件夹的应用中有一下两层好处.
例如,我在同事的移动硬盘里找到了一些电子书,想把它们复制到F盘中的学习资料文件夹.在复制这些电子书的时候,我并不需要考虑这批文件的类型,不管它们是单独的电子书还是被放在了文件夹中.组合模式让Ctrl+V,Ctrl+C成为了一个统一的操作.
当我用杀毒软件扫描该文件夹时,往往不会关心里面有多少文件和子文件夹,组合模式使得我们只需要操作最外层的文件夹进行扫描
现在我们来编写代码,首先分别定义好文件夹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
/***********Folder***********/
var Folder=function(name){
this.name=name;
this.files=[];
}
Folder.prototype.add=function(file){
this.files.push(file)
}
Folder.prototype.scan=function(){
console.log("开始扫描文件夹:"+this.name);
for(var i=0;file=this.files[i];i++){
file.scan();
}
}
/***********File***********/
var File=function(name){
this.name=name;
}
File.prototype.add=function(){
throw new Error("文件下面不能添加文件");
}
File.prototype.scan=function(){
console.log("开始扫描文件:"+this.name);
}

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

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

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

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

通过这个例子,我们再次看到客户是如何同等对待组合对象和叶对象.在添加一批文件的操作过程中,客户不用分辨它们到底是文件还是文件夹.新增加的文件和文件夹能够很容易地添加到原来的树结构中,和树里已有的对象一起工作
我们改变了树的结构,添加了新的数据,却不用修改任何一句原有的代码,这是符合开放-封闭原则的.
运用了组合模式之后,扫描整个文件夹的操作也是轻而易举的,我们只需要操作树的最顶端对象

folder.scan()

执行结果如下如所示:

一些值得注意的地方

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