軟體設計

Swift Composite Pattern:將樹狀結構當作單一物件處理

資料夾裡有資料夾,裡面又有檔案。只要曾經用程式碼處理過這種樹狀結構,應該多少都感到過棘手。

閱讀 4 分鐘
Swift Composite Pattern:將樹狀結構當作單一物件處理 封面圖

資料夾裡有資料夾,裡面又有檔案。只要曾經用程式碼處理過這種樹狀結構,應該多少都感到過棘手。

每次都用if區分「這是資料夾還是檔案?」,程式碼很快就會變得凌亂。

製作類似檔案總管的功能時,這是很常見的地獄。這時可以拿來使用的工具,就是 Composite Pattern。

Composite Pattern 將個別物件(檔案)與其集合(資料夾)視為相同型別,讓你能像處理單一物件一樣處理整棵樹。

今天就搭配我親自撰寫的程式碼,整理如何用 Swift 實作這個模式。

先看重點總結

  1. Composite Pattern=用同一個協定 объедин Leaf 與 Composite
  2. 用戶端不必區分檔案或資料夾,直接以相同方式呼叫
  3. 資料夾的計算會再次詢問子物件,形成遞迴結構
  4. 在 Swift 中只要一個protocol就能簡潔實作

什麼是 Composite Pattern?

就字面意思來說,就是「組合」:把小東西集合起來,形成一個更大的整體。

最常見的比喻就是檔案系統。

單一檔案本身就是終點,也就是葉節點。相對地,資料夾則是可以容納檔案或其他資料夾的分支。

資料夾裡有資料夾,這正是 Composite
資料夾裡有資料夾,這正是 Composite

但使用者的角度不太一樣。不論開啟資料夾或檔案,我們都只會下達「開啟」的指令。

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()即可,其他部分會由各個物件自行處理。

我把範例原樣貼到 Playground 中執行了
我把範例原樣貼到 Playground 中執行了

這和使用分支敘述處理有什麼不同?

你可能會想:「直接用if敘述區分型別來處理,不行嗎?」

可以,只是樹越深,差異就越明顯。

分類 分支敘述(if/switch)方式 Composite Pattern
型別判斷 每次都自行確認 不需要
新增型別 到處修改分支敘述 採用協定
遞迴處理 自行撰寫巡覽程式碼 由物件自行處理
程式碼可讀性 越深越複雜 維持一致

分支方式遇到新種類(例如捷徑連結)時,還得在各處再次加入if

相對地,Composite 只要建立一個採用FileComponent的新型別就完成了,不必修改既有程式碼。


常見問題

Q. Leaf 與 Composite 的介面必須完全相同嗎?

理想上是的。不過像「新增子項目」這類功能對檔案來說很不自然。此時只要把共用行為(例如 size)放在協定中,子項目管理則只交給資料夾負責,會比較實用。

Q. 可以不用class,改用struct嗎?

可以,如同上面的範例,使用值型別也能正常運作。不過如果經常修改樹狀結構,且需要共享參考,class可能會更方便。


如果你在樹狀結構面前陷入if地獄,不妨試著拿出 Composite Pattern。

除了檔案系統之外,只要是檢視階層、選單、組織圖等「裡面還有一層」的結構,都很適合。

把今天的範例原樣貼到 Playground 執行,馬上就能掌握概念。

延伸閱讀