7_组合模式
1 简介
- 用小的子对象来构建更大的对象,而这些小的子对象本身也许是由更小的“孙对象”构成的
回顾宏命令
marcoCommand
被称为组合对象closeDoorCommand
、openPcCommand
、openQQCommand
都是叶对象
marcoCommand
的execute
并不执行真正的操作- 而是将真正的
execute
请求委托给叶对象
- 而是将真正的
marcoCommand
作为代理,只负责传递请求
宏命令中包含了一组子命令,它们组成了一个树形结构,这里是一棵结构非常简单的树
2 组合模式的用途
1. 表示树形结构
- 通过调用组合对象的
execute
方法,程序会递归调用组合对象下面的叶对象的execute
方法
方便地描述对象
部分-整体
层次结构
2. 利用对象多态性,统一对待组合对象和单个对象
3 更强大的宏命令
实现超级万能遥控,能控制家里所有电器,有如下功能
- 打开空调
- 打开电视和音箱
- 关门、开电脑、登录QQ
3.1 定义宏命令
class MacroComamnd {
constructor() {
this.commandsList = []
}
add(command) {
this.commandsList.push(command)
}
execute() {
this.commandsList.map(command => command.execute())
}
}
3.2 命令A
let openAcCommand = {
execute() {
console.log('打开空调');
}
}
3.3 命令B
let openTVCommand = {
execute() {
console.log('打开电视');
}
}
let openSoundCommand = {
execute() {
console.log('打开音响');
}
}
let macroCommand1 = new MacroComamnd()
macroCommand1.add(openTVCommand)
macroCommand1.add(openSoundCommand)
3.4 命令C
let closeDoorCommand = {
execute() {
console.log('关门');
}
}
let openPcCommand = {
execute() {
console.log('开电脑');
}
}
let openQQCommand = {
execute() {
console.log('登录QQ');
}
}
let macroCommand2 = new MacroComamnd()
macroCommand2.add(closeDoorCommand)
macroCommand2.add(openPcCommand)
macroCommand2.add(openQQCommand)
3.5 超级命令
let macroCommand = new MacroComamnd()
macroCommand.add(openAcCommand)
macroCommand.add(macroCommand1)
macroCommand.add(macroCommand2)
3.6 事件绑定
let setCommand = (command => {
document.getElementById('btn').onclick = () => {
command.execute()
}
})(macroCommand)
- 基本对象可以被组合成更复杂的组合对象,组合对象又可以被组合 --> 形成树结构
- 调用最上层对象的
execute()
就能让整棵树运转起来
每当对最上层对象进行一次请求,实际上是对整棵树进行深度优先的搜索
4 抽象类在组合模式中的作用
- 可以一致地对待组合对象和基本对象
5 透明性带来的安全问题
- 误操作时需要抛出异常
let openTvCommand = {
execute() {
console.log('打开电视');
},
add() {
throw new Error('叶对象不能添加子节点')
}
}
let macroCommand = new MacroCommand()
macroCommand.add(openTvCommand)
openTvCommand.add(macroCommand)
6 组合模式的例子 -- 扫描文件夹
文件夹里既可以包含文件,又可以包含其它文件夹,最终组成一棵树
6.1 定义Folder类
class Folder {
constructor(name) {
this.name = name
this.files = []
}
add(file) {
this.files.push(file)
}
scan() {
console.log('开始扫描文件夹' + this.name)
this.files.map(file => file.scan())
}
}
6.2 定义File类
class File {
constructor(name) {
this.name = name
}
add() {
throw new Error('文件下面不能再添加文件')
}
scan() {
console.log('开始扫描文件' + this.name)
}
}
6.3 应用
let folder = new Folder('学习资料')
let folder1 = new Folder('js')
let folder2 = new Folder('vue')
let file1 = new File('学习资料文件')
let file2 = new File('js文件')
let file3 = new File('vue文件')
folder1.add(file1)
folder2.add(file2)
folder.add(folder1)
folder.add(folder2)
folder.add(file3)
folder.scan()
7 一些值得注意的地方
1. 组合模型不是父子关系
组合模型是一种聚合关系
- 组合对象把请求委托给它所包含的所有叶对象
- 组合对象与叶对象能够合作的关键是拥有相同的接口
2. 对叶对象操作的一致性
3. 双向映射关系
- 某位架构师既隶属于开发组,又隶属于架构组,对象之间的关系并不是严格意义上的层次结构
- 此时必须给父节点和子节点建立双向映射关系,一个简单的方法是给小组和员工对象都增加集合来保存对方的引用
- 但是这种相互间的引用相当复杂,而且对象之间产生了过多的耦合性,修改或者删除一个对象都变得困难,此时我们可以引入中介者模式来管理这些对象
4. 用职责链模式提高组合模式性能
- 借用职责链模式 避免遍历整棵树
8 引用父对象
- this.parent
有时候我们需要在子节点上保持对父节点的引用,比如在组合模式中使用职责链时,有可能需要让请求从子节点往父节点上冒泡传递。还有当我们删除某个文件的时候,实际上是从这个文件所在的上层文件夹中删除该文件的
8.1 改写Folder类
class Folder {
constructor(name) {
this.name = name
this.parent = null
this.files = []
}
add(file) {
this.parent = this
this.files.push(file)
}
scan() {
console.log('开始扫描文件夹' + this.name)
this.files.map(file => file.scan())
}
remove() {
// 根节点或者树外的游离节点
if(!this.parent) return
let files = this.parent.files;
for(let i = files.length-1; i >= 0; i--) {
if(files[i] = this) {
files.splice(i, 1)
}
}
}
}
8.2 改写File类
class File {
constructor(name) {
this.name = name
this.parent = null
}
add() {
throw new Error('文件下面不能再添加文件')
}
scan() {
console.log('开始扫描文件' + this.name)
}
remove() {
// 根节点或者树外的游离节点
if(!this.parent) return
let files = this.parent.files;
for(let i = files.length-1; i >= 0; i--) {
if(files[i] = this) {
files.splice(i, 1)
}
}
}
}
8.3 应用
let folder = new Folder('学习资料')
let folder1 = new Folder('js')
let file1 = new File('vue文件')
folder1.add(new File('js设计模式'))
folder.add(file1)
folder1.remove()
folder.scan()
9 何时使用组合模式
- 表示对象的部分-整体层次结构
- 客户希望统一对待树中的所有对象