1、什么是组合模式
在程序设计中,有一些**“事物是由相似的子事物构成”**。组合模式就是用小的事物来构建更大的对象,而这些小的事物本身也许是由更小的“孙对象”构成。
比如在命令模式中,宏命令对象中包含了一组具体的子命令对象,不管是宏命令对象还是子命令对象,都有一个execute
方法负责执行命令。
2、组合模式的用途
组合模式将对象组合成树形结构,以表示**“部分-整体”的层次结构**。组合模式另一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性。
- 树形结构:我们在通过组合对象调用的
execute
方法,程序会自动递归调用组合对象下面的叶子对象的execute
方法。 - 对象的多态性表现:即我们可以忽略组合对象和单个对象的不同。可以一致的对待组合对象和单个对象,即不必区分组合对象和单个对象。
例子
<button id="btn">click</button>
<script>
let MacroCommand = function () {
return {
commandList: [],
add(command) {
this.commandList.push(command)
},
execute() {
this.commandList.forEach(command => {
command.execute()
});
}
}
}
let open1 = {
execute() {
console.log(1)
}
}
let open2 = {
execute() {
console.log(2)
}
}
let open3 = {
execute() {
console.log(3)
}
}
let open4 = {
execute() {
console.log(4)
}
}
let macroCommand = MacroCommand()
let macroCommand1 = MacroCommand() // 添加到组合对象中的组合对象
macroCommand.add(open1)
macroCommand.add(open2)
macroCommand1.add(open3)
macroCommand1.add(open4)
macroCommand.add(macroCommand1)
let setCommand = (function (command) {
document.getElementById('btn').addEventListener('click', function () {
command.execute()
})
})(macroCommand)
// 在上面的这个例子中我们在组合对象中不仅添加了单个对象,也添加组合对象,但是我们只需调用最外面的组合对象,内部的单个对象和组合对象都会被递归调用
</script>
在上面的·这个例子中我们首先创建了一个组合对象,并且在组合对象中添加了单个对象和组合对象,但是我们只需要调用最外层的组合对象的execute
方法,就会自动的递归调用组合对象中的子对象,不管子对象是单个对象还是一个组合对象,这既体现了组合模式的树形结构又体现了组合模式的对象的多态性表现。
3、和静态语言的比较
在静态语言中,我们的组合对象和单个对象都需要继承自一个对象,但是在JavaScript中对象的多态性是与生俱来的,编译器不会去检查变量的类型,只要添加的具有execute
方法,这就需要我们使用鸭子类型的思想对它们进行接口检查。
4、透明性带来的安全性问题
我们只可以在组合对象中添加新的子对象,但是不能在单个对象中添加新的子对象,所以我们应该在向单个对象添加新的对象的时候抛出一个错误。
5、组合模式的例子
1、文件夹与文件之间的关系
<script>
let Folder = function (name) {
this.name = name
this.files = []
}
Folder.prototype.add = function (file) {
this.files.push(file)
}
Folder.prototype.scan = function () {
console.log(`start scan floder ${this.name}`)
this.files.forEach(file => {
file.scan()
});
}
let File = function (name) {
this.name = name
}
File.prototype.add = function () {
throw new Error("文件下不能再添加文件")
}
File.prototype.scan = function () {
console.log(`start scan file ${this.name}`)
}
let folder1 = new Folder("文件夹1")
let folder2 = new Folder("文件夹2")
let folder3 = new Folder("文件夹3")
let file1 = new File("文件1")
let file2 = new File("文件2")
let file3 = new File("文件3")
folder1.add(file1)
folder2.add(file2)
folder3.add(file3)
folder1.add(folder2)
folder1.add(folder3)
folder1.scan()
在上面的这个例子中,我们首先创建了一个文件夹的类,然后定义了一个add
方法向文件夹中添加新的文件或者文件,然后定于了一个scan
方法来扫描文件夹中的内容,然后调用files
中的scan
方法;然后定义了一个文件类,也定义了文件类的扫描方法。
6、注意
- 组合模式不是父子关系,组合模式是一种HAS-A(聚合)关系,
- 对叶对象操作的一致性
- 双映射关系
- 用指责连模式提高组合模式的性能
7、引用父对象
组合对象保存了对子对象的引用,但是有些情况需要子对象保存对父对象的引用,比如在删除文件的时候。
let 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.scan = function () {
console.log(`开始扫面文件夹${this.name}`)
this.files.forEach(file => {
file.scan()
});
}
Folder.prototype.remove = function () {
if (!this.parent) {
return
}
for (let files = this.parent.files, i = files.length - 1; i >= 0; i--) {
let file = files[i]
if (file === this) {
files.splice(i, 1)
}
}
}
let 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.remove = function () {
if (!this.parent) {
return
}
for( let files = this.parent.files, i = files.length; i>=0; i-- ) {
let file = files[i]
if (file === this) {
files.splice(i, 1)
}
}
}
let folder = new Folder("学习资料")
let folder1 = new Folder("Javascript")
let file1 = new File("深入浅出Node.js")
folder1.add(new File("JavaScript设计模式与开发"))
folder.add(folder1)
folder.add(file1)
folder1.remove()
folder.scan()
8、什么时候使用组合模式
- 表示对象的部分-整体结构层次结构
- 客户希望统一对待树中的所有对象