資料夾裡有資料夾,裡面又有檔案。只要曾經用程式碼處理過這種樹狀結構,應該多少都感到過棘手。
每次都用if區分「這是資料夾還是檔案?」,程式碼很快就會變得凌亂。
製作類似檔案總管的功能時,這是很常見的地獄。這時可以拿來使用的工具,就是 Composite Pattern。
Composite Pattern 將個別物件(檔案)與其集合(資料夾)視為相同型別,讓你能像處理單一物件一樣處理整棵樹。
今天就搭配我親自撰寫的程式碼,整理如何用 Swift 實作這個模式。
先看重點總結
- Composite Pattern=用同一個協定 объедин Leaf 與 Composite
- 用戶端不必區分檔案或資料夾,直接以相同方式呼叫
- 資料夾的計算會再次詢問子物件,形成遞迴結構
- 在 Swift 中只要一個
protocol就能簡潔實作
什麼是 Composite Pattern?
就字面意思來說,就是「組合」:把小東西集合起來,形成一個更大的整體。
最常見的比喻就是檔案系統。
單一檔案本身就是終點,也就是葉節點。相對地,資料夾則是可以容納檔案或其他資料夾的分支。
但使用者的角度不太一樣。不論開啟資料夾或檔案,我們都只會下達「開啟」的指令。
Composite Pattern 的目標,就是讓個別元素與集合能以相同方式處理。
組成元件分成三種。
- Component:共用介面(協定)
- Leaf:沒有子項目的末端物件(檔案)
- Composite:容納子項目的物件(資料夾)
如何用 Swift 實作?
先定義共用協定,讓我們可以詢問名稱與大小。
以下是檔案與資料夾共同遵循的規則(Component)。
protocol FileComponent {
var name: String { get }
func size() -> Int // 回傳位元組大小
}
接著是作為葉節點的檔案。只要回傳自己的大小即可,非常單純。
struct File: FileComponent {
let name: String
let bytes: Int
func size() -> Int { bytes } // 原樣回傳自己的大小
}
重點在資料夾(Composite)。它以陣列容納子項目,收到大小詢問時會再次詢問子項目。
struct Folder: FileComponent {
let name: String
var children: [FileComponent] = []
func size() -> Int {
children.reduce(0) { $0 + $1.size() } // 加總子項目(遞迴)
}
}
重點是size()會再次呼叫自己,形成遞迴結構。
不論資料夾嵌套多少層,這一行都會自動一路走到底。
實際使用時會是這樣
現在來混合檔案與資料夾,建立一棵樹。
let root = Folder(name: "文件", children: [
File(name: "備忘錄.txt", bytes: 100),
Folder(name: "照片", children: [
File(name: "旅遊.jpg", bytes: 2000)
])
])
print(root.size()) // 2100 輸出
看到了嗎?只要呼叫一次root.size(),就會加總資料夾內所有檔案的大小。
這是我親自執行後最驚嘆的地方。
用戶端程式碼一次也不必詢問「這是資料夾還是檔案」。
只要呼叫size()即可,其他部分會由各個物件自行處理。
這和使用分支敘述處理有什麼不同?
你可能會想:「直接用if敘述區分型別來處理,不行嗎?」
可以,只是樹越深,差異就越明顯。
| 分類 | 分支敘述(if/switch)方式 | Composite Pattern |
|---|---|---|
| 型別判斷 | 每次都自行確認 | 不需要 |
| 新增型別 | 到處修改分支敘述 | 採用協定 |
| 遞迴處理 | 自行撰寫巡覽程式碼 | 由物件自行處理 |
| 程式碼可讀性 | 越深越複雜 | 維持一致 |
分支方式遇到新種類(例如捷徑連結)時,還得在各處再次加入if。
相對地,Composite 只要建立一個採用FileComponent的新型別就完成了,不必修改既有程式碼。
常見問題
Q. Leaf 與 Composite 的介面必須完全相同嗎?
理想上是的。不過像「新增子項目」這類功能對檔案來說很不自然。此時只要把共用行為(例如 size)放在協定中,子項目管理則只交給資料夾負責,會比較實用。
Q. 可以不用class,改用struct嗎?
可以,如同上面的範例,使用值型別也能正常運作。不過如果經常修改樹狀結構,且需要共享參考,class可能會更方便。
如果你在樹狀結構面前陷入if地獄,不妨試著拿出 Composite Pattern。
除了檔案系統之外,只要是檢視階層、選單、組織圖等「裡面還有一層」的結構,都很適合。
把今天的範例原樣貼到 Playground 執行,馬上就能掌握概念。

