[TOC]
# 组合模式
组合模式是将对象组合成树形结构,以表示“部分-整体”的层次结构。
在大多数情况下,我们都可以忽略掉组合对象和单个对象之间的差别,从而用一致的方式来处理它们。
- 表示树形结构:提供了一种遍历树形结构的方案,通过调用组合对象的execute方法,程序会递归调用组合对象下面的叶对象的execute方法。组合模式可以非常方便地描述对象部分-整体层次结构。
- 利用对象多态性统一对待组合对象和单个对象
## 请求在树中传递的过程
请求从树最顶端的对象往下传递,当前处理请求的对象是叶对象的话,叶对象自身会对请求做出相应的处理;如果当前请求的对象是组合对象,组合对象则会遍历它属下的子节点,将请求继续传递给这些子节点。
总而言之,子节点是叶对象,叶对象自身会处理这个请求,而如果子节点还是组合对象,请求会继续往下传递。
## 更强大的宏命令
```javascript
var MacroCommand = function(){
return {
commandList: [],
add: function(command){
this.commandsList.push(command);
},
execute: function(){
for(var i=0, command; command = this.commandList[i++];){
command.execute();
}
}
}
};
var openAcCommand = {
execute: function(){
...
}
};
// Tv、Sound 是相连的
var openTvCommand = {
execute: function(){
...
}
};
var openSoundCommand = {
execute: function(){
...
}
};
var macroCommand1 = MacroCommand();
macroCommand1.add(openTvCommand);
macroCommand1.add(openSoundCommand);
// closeDoor、openPC、QQ 是相连的
var closeDoorCommand = {
execute: function(){
...
}
};
var openPCCommand = {
execute: function(){
...
}
};
var openQQCommand = {
execute: function(){
...
}
};
var macroCommand2 = MacroCommand();
macroCommand2.add(closeDoorCommand);
macroCommand2.add(openPCCommand);
macroCommand1.add(openQQCommand);
var macroCommand = MacroCommand();
macroCommand.add(openAcCommand);
macroCommand.add(macroCommand1);
macroCommand.add(macroCommand2);
macroCommand.execute();
```
基本对象可以被组合成更复杂的组合对象,组合对象又可以被组合,这样不断递归下去,这棵树的结构可以支持任意多的复杂度。实际上是对整个树进行深度优先的搜索。
## 抽象类在组合模式中的作用
在`JavaScript`这种动态语言中,对象的多态性是与生俱来的。`JavaScript`中实现组合模式的难点在于要保证组合对象和叶对象拥有同样的方法,通常需要用鸭子类型的思想对它们进行接口检查。
## 透明性带来的安全问题
组合对象可以拥有子节点,叶对象下面就没有子节点,所以会发生一些误操作。解决方案通常从是也给叶对象增加对应的方法,并且在调用的时候抛出异常。
```javascript
var MacroCommand = function(){
return {
commandList: [],
add: function(command){
this.commandsList.push(command);
},
execute: function(){
for(var i=0, command; command = this.commandList[i++];){
command.execute();
}
}
}
};
var openAcCommand = {
execute: function(){
...
},
add: function(){ // 增加相同的方法并抛出异常
throw new Error('叶对象不能添加子节点');
}
};
```
## 一些值得注意的地方
- 组合模式不是父子关系:组合模式是一种`HAS-A 聚合`的关系,不是`IS-A`。
- 对叶对象操作的一致性:对一组叶对象的操作必须具有一致性,只有用一致的方法对待列表中的每个叶对象的时候,才适合使用组合模式。
- 双向映射关系:引入中介者模式来管理。
- 用职责链模式提高组合模式性能:组合模式中,父对象和子对象之间实际上形成了天然的职责链。让请求顺着链条从父对象往子对象传递,或者是反过来从子对象往父对象传递,直到遇到可以处理该请求的对象为止。
## 引用父对象
有时需要在子节点上保持对父节点的引用,实际上是从子节点的父节点中查找该节点。
```javascript
var Folder = function(name){
this.name = name;
this.parent = null;
this.files = [];
};
Folder.prototype.add = function(file){
file.parent = this; // 设置父对象
this.files.push(file);
};
Folder.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);
}
}
};
```
首先会判断`this.parent`,其次再遍历父节点中保存的子节点列表,找到先要删除的子节点。
## 何时使用组合模式
- 表示对象的部分-整体层次结构:在开发期间不确定这棵树到底存在多说层次的时候,在树的构造最终完成之后,只需要通过请求树的最顶层对象,便能对整棵树做统一的操作。
- 客户希望统一对待树中的所有对象。组合模式使客户可以忽略组合对象和叶对象的区别,不用关心当前正在处理的对象是组合对象或者叶对象,不需要写一堆`if`、`else`语句来分别处理它们。
在组合模式中可能会产生这样一个系统:系统中的每个对象看起来都与其它对象差不多。它们的区别只有在运行的时候才会显现出来,这会使代码难以理解。此外,如果通过组合模式创建了太多的对象,那么这些对象可能会让系统负担不起。