js常用设计模式6-组合模式
组合模式和命令模式有点像,命令模式是一个个小的指令,而组合模式是一些小指令组合成的大指令
1,命令模式和组合模式的联合应用
试想这么一个场景:我们回家之后先关门,然后开电脑,最后打开QQ
关门,开电脑,开QQ是三个命令,现在我们用MacroCommand函数把他们组合起来,得到一个对象macroCommand ,通过macroCommand 来操作所有的命令。
- macroCommand 被称作组合对象,它实际上是真正的命令数组commandList的“代理”。当然macroCommand不是代理,它只负责传递请求给真正的命令函数。
- 关门,开电脑,开QQ都是叶对象。
var closeDoorCommand = {
execute: function () {
console.log('关门')
}
}
var openPCCommand = {
execute: function () {
console.log('开电脑')
}
}
var openQQCommand = {
execute: function () {
console.log('开QQ')
}
}
var MacroCommand = function () {
return {
commandList: [],
add: function (command) {
this.commandList.push(command)
},
execute: function () {
for (var i = 0, command; command = this.commandList[i++];) {
command.execute()
}
}
}
}
var macroCommand = MacroCommand()
//宏命令包含了一组子命令,形成了树形结构
macroCommand.add(closeDoorCommand)
macroCommand.add(openPCCommand)
macroCommand.add(openQQCommand)
//macroCommand:它是一个组合对象,表现为命令,但实际上只是一组真正命令的代理
macroCommand.execute()
2,分析一下组合模式
在上面的例子中macroCommand是 closeDoorCommand、openPCCommand、openQQCommand这三个命令的组合对象,它们有一个共同点:都有execute函数。这个函数代表了组合和单个命令的一致性。
组合模式将对象组合成树形结构,以表示“部分-整体”的结构层次,加上execute实现的一致性,使得用户在使用的时候,可以忽略组合和单个命令的不同,直接调用就完事了。
对于一个遥控器而言,当我们按下一个键时,只关注它带来的结果,而不需要在意这个操作调用了多少命令,只要它有execute,那么它就是好命令。
3,更强大的宏命令
现在我们的遥控器,包含了关门、开电脑、开QQ这三个功能。现在我们需要一个超级遥控器,能控制家里所有的电器,包括:
- 打开空调
- 打开电视和音响
- 关门、开电脑、开QQ
这时候我们会发现,之前的macroCommand现在变成了一个组合对象的一部分。
/**
* 正题
* 组合模式就是组合了一堆命令,可以统一调用,而忽略单个命令
*/
//更强大的宏命令--只要有execute,你就是他的一员,进行深度遍历
var MacroCommand = function () {
return {
commandList: [],
add: function (command) {
this.commandList.push(command)
},
execute: function () {
for (var i = 0, command; command = this.commandList[i++];) {
command.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)
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 macroCommand = MacroCommand()
macroCommand.add(openAcCommand)
macroCommand.add(macroCommand1)
macroCommand.add(macroCommand2)
macroCommand.execute()
错误处理:
//缺点:叶节点可能会使用add方法,需要错误处理
var openAcCommand = {
execute: function () {
console.log('开空调')
},
add: function () {
throw new Error('叶节点不能添加子对象')
}
}
openAcCommand.add()
4,组合模式的实例-扫描文件夹
文件夹和文件之间的联系,非常适合用组合模式来描述(个人觉得dom节点的关系也很适合)。文件夹里既可以包含文件,又可以包含其他文件夹,最终形成了一棵树。组合模式对于文件夹应用有以下两个好处:
- 复制文件夹所有内容的时候,只需要复制最外层的文件夹就行了
- 用杀毒软件扫描文件夹的时候,不需要关心文件夹里面有多少文件夹或者文件,直接扫描最上层文件夹就可以了。
现在,我们先定义文件夹Folder和文件File这两个类:
/*********** 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; file = this.files[i++];) {
file.scan()
}
}
/*********** Folder ***********/
var File = function (name) {
this.name = name
}
File.prototype.add = function () {
throw new Error('文件下面不能添加文件')
}
File.prototype.scan = function () {
console.log('开始扫描文件:' + this.name)
}
然后,创建文件夹和文件,将其组合成一棵树,这个结构就是我们硬盘里的文件目录结构:
var mainFolder = new Folder('学习资料')
var folder1 = new Folder('vue资料')
var folder2 = new Folder('react资料')
var file1 = new File('vue api')
var file2 = new File('vue 生命周期')
var file3 = new File('react-router')
var file4 = new File('设计模式')
folder1.add(file1)
folder1.add(file2)
folder2.add(file3)
mainFolder.add(folder1)
mainFolder.add(folder2)
mainFolder.add(file4)
mainFolder.scan()
通过这个例子我们可以看到,当我们需要遍历整个文件夹时,只需要调用最上层文件夹的scen方法:mainFolder.scan()。在新增文件时,用户也不需要关心它们具体是文件夹还是文件,直接添加进去就完事了。
5,一些需要注意的地方
- 组合模式是聚合关系,而不是父子关系,因为叶对象(最开始的单个命令)不是组合对象的子类。组合对象可以把请求委托给它的所有叶对象,因为它们有相同的接口(和dom树很像)。
- 组合模式的应用场景:必须要每个节点都有相同的接口,以及操作一致性。比如之前的例子,现在每个文件节点都有scan方法,但是如果有的文件有删除方法,有的没有,那么组合模式就不适用,要么都有,那么都没有。
- 双向映射关系:一个节点只能属于一个组合对象,不能同时属于两个,比如一个文件,只能有一个直接的文件夹来包含,不可能同时有两个。如果一个人属于开发组,同时又属于测试组,这种交叉情况就不适用于组合模式。
- 组合模式的对象关系和职责链模式很像。
6,叶对象引用父对象
之前的例子中,只能从父对象到叶对象,反过来是不行的。但是,当我们要删除某个文件的时候,我们需要知道它具体属于哪个文件夹,实际是从上层文件夹中删除文件的。
首先改写Folder类和File类,增加parent属性,在add函数中设置parent:
var Folder = function (name) {
this.name = name
this.files = []
}
Folder.prototype.add = function (file) {
//添加父节点引用
file.parent = this
this.files.push(file)
}
Folder.prototype.scan = function () {
console.log('开始扫描文件夹:' + this.name)
for (var i = 0, file; file = this.files[i++];) {
file.scan()
}
}
给文件夹添加删除功能。
如果this.parent === null,要么它就是根节点,要么是还没有添加到树中,这种情况先暂时return,不作处理。
否则的话,该文件夹有父节点,那么就遍历父节点的所有子节点,找个需要删除的子节点,直接删除。
//添加删除功能。
Folder.prototype.delete = function () {
if (this.parent === null) {
return
}
for (var i = 0, files = this.parent.files; file = files[i]; i++) {
if (file === this) {
files.splice(i, 1)
}
}
}
File类的实现基本一致:
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.delete = function () {
if (this.parent === null) {
return
}
for (var i = 0, files = this.parent.files; file = files[i]; i++) {
if (file === this) {
files.splice(i, 1)
}
}
}
最后,我们来try一try:
var folder = new Folder('学习资料')
var folder1 = new Folder('vue')
var folder2 = new Folder('react')
var file1 = new File('vue api')
var file2 = new File('react api')
var file3 = new File('设计模式')
folder1.add(file1)
folder2.add(file2)
folder.add(folder1)
folder.add(folder2)
folder.add(file3)
folder1.delete()
folder.scan()
7,小结
何时使用组合模式:
- 表示对象的“部分-整体”结构层次。组合模式构造了一棵树,来表示对象的“部分-整体”结构,用户不需要知道树到底有多少层,只需要请求最顶层的节点,就可以对整棵树做统一的操作。
- 树中的所有对象都一致。客户不需要关注一个节点到底是组合对象还是叶对象,因为它们有相同的方法,能用就完事了。