概念
将对象组合成树形结构以表示‘部分-整体’的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
应用
虚拟DOM
Vue、React用render函数渲染虚拟DOM。
Document
宏命令
宏命令对象包含了一组具体的子命令对象,不管是宏命令对象,还是子命令对象,都有一个execute方法负责执行命令。
let closeDoorCommand = {
execute: function(){
console.log( '关门' )
}
}
let openPcCommand = {
execute: function(){
console.log( '开电脑' )
}
}
let openQQCommand = {
execute: function(){
console.log( '登录QQ' )
}
}
let MacroCommand = function(){
return {
commandsList: [],
add: function( command ){
this.commandsList.push( command )
},
execute: function(){
for ( let i = 0, command; command = this.commandsList[ i++ ]; ){
command.execute()
}
}
}
}
let macroCommand = MacroCommand()
macroCommand.add( closeDoorCommand )
macroCommand.add( openPcCommand )
macroCommand.add( openQQCommand )
macroCommand.execute()
宏命令中包含了一组子命令,它们组成了一个树形结构,这里是一棵结构非常简单的树。
其中,marcoCommand被称为组合对象,closeDoorCommand、openPcCommand、openQQCommand都是子对象。在macroCommand的execute方法里,并不执行真正的操作,而是遍历它所包含的子对象,把真正的execute请求委托给这些子对象。macroCommand表现得像一个命令,但它实际上只是一组真正命令的“代理”。并非真正的代理,虽然结构上相似,但macroCommand只负责传递请求给子对象,它的目的不在于控制对子对象的访问。
组合模式将对象组合成树形结构,以表示“部分——整体”的层次结构。除了用来表示树形结构之外,组合模式的另一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性。
1、表示树形结构。组合模式的优点:提供了一种遍历树形结构的方案,通过调用组合对象的execute方法,程序会递归调用组合对象下面的子对象的execute方法。组合模式可以非常方便地描述对象部分——整体层次结构。
2、利用对象多态性统一对待组合对象和单个对象。利用对象的多态性表现,可以使客户端忽略组合对象和单个对象的不同。在组合模式中,客户将统一地使用组合结构中的所有对象,而不需要关心它究竟是组合对象还是单个对象。
扫描文件夹
文件夹和文件之间的关系,非常适合用组合模式来描述。文件夹里既可以包含文件,又可以包含其他文件夹,最终可能组合成一棵树,组合模式在文件夹的应用中有以下两层好处。
1. 组合模式让Ctrl+V、Ctrl+C成为了一个统一的操作。例如,在移动硬盘里找到了一些电子书,想把它们复制到F盘中的学习资料文件夹。在复制这些电子书的时候,并不需要考虑这批文件的类型,不管它们是单独的电子书还是被放在了文件夹中。
2. 用杀毒软件扫描该文件夹时,往往不会关心里面有多少文件和子文件夹,组合模式使得只需要操作最外层的文件夹进行扫描。
现在来编写代码,首先分别定义好文件夹Folder和文件File这两个类。见如下代码:
let 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 ( let i = 0, file, files = this.files; file = files[ i++ ]; ){
file.scan()
}
}
let File = function( name ){
this.name = name
}
File.prototype.add = function(){
throw new Error( '文件下面不能再添加文件' )
}
File.prototype.scan = function(){
console.log( '开始扫描文件: ' + this.name )
}
接下来创建一些文件夹和文件对象,并且让它们组合成一棵树,这棵树就是F盘里的现有文件目录结构。
let folder = new Folder( '学习资料' )
let folder1 = new Folder( 'JavaScript' )
let folder2 = new Folder ( 'jQuery' )
let file1 = new File( 'JavaScript 设计模式与开发实践' )
let file2 = new File( '精通jQuery' )
let file3 = new File( '重构与模式' )
folder1.add( file1 )
folder2.add( file2 )
folder.add( folder1 )
folder.add( folder2 )
folder.add( file3 )
现在的需求是把移动硬盘里的文件和文件夹都复制到这棵树中,假设已经得到了这些文件对象:
let folder3 = new Folder( 'Nodejs' )
let file4 = new File( '深入浅出Node.js' )
folder3.add( file4 )
let file5 = new File( 'JavaScript 语言精髓与编程实践' )
接下来就是把这些文件都添加到原有的树中:
folder.add( folder3 )
folder.add( file5 )
通过这个例子,再次看到客户是如何同等对待组合对象和基本对象的。在添加一批文件的操作过程中,客户不用分辨它们到底是文件还是文件夹。新增加的文件和文件夹能够很容易地添加到原来的树结构中,和树里已有的对象一起工作。改变了树的结构,增加了新的数据,却不用修改任何一句原有的代码,这是符合开放——封闭原则的
运用了组合模式之后,扫描整个文件夹的操作也是轻而易举的,只需要操作树的最顶端对象:
folder.scan()
总结
组合模式如果运用得当,可以大大简化客户的代码。一般来说,组合模式适用于以下这两种情况。
1. 表示对象的部分——整体层次结构。组合模式可以方便地构造一棵树来表示对象的部分——整体结构。特别是在开发期间不确定这棵树到底存在多少层次的时候。在树的构造最终完成之后,只需要通过请求树的最顶层对象,便能对整棵树做统一的操作。在组合模式中增加和删除树的节点非常方便,并且符合开放——封闭原则。
2. 客户希望统一对待树中的所有对象。组合模式使客户可以忽略组合对象和子对象的区别,客户在面对这棵树的时候,不用关心当前正在处理的对象是组合对象还是子对象,也就不用写一堆if、else语句来分别处理它们。组合对象和子对象会各自做自己正确的事情,这是组合模式最重要的能力。
然而,组合模式并不是完美的,它可能会产生一个这样的系统:系统中的每个对象看起来都与其他对象差不多。它们的区别只有在运行的时候会才会显现出来,这会使代码难以理解。此外,如果通过组合模式创建了太多的对象,那么这些对象可能会让系统负担不起。
参考文献
《JavaScript设计模式与开发实践》
《大话设计模式》