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、什麼時候使用組合模式
- 表示對象的部分-整體結構層次結構
- 客戶希望統一對待樹中的所有對象