フォルダーの中にフォルダー、その中にさらにファイル。こうしたツリー構造をコードで扱ったことがあるなら、一度は途方に暮れたはずです。
「これはフォルダー?それともファイル?」と毎回ifで分岐していると、コードはすぐに汚れてしまいます。
ファイルエクスプローラーのようなものを作ると、よく直面する問題です。そんなときに使えるのがCompositeパターンです。
Compositeパターンは、個々のオブジェクト(ファイル)とその集合(フォルダー)を同じ型として扱い、ツリー全体を1つのオブジェクトのように操作できる構造パターンです。
今日は、私が実際に書いたコードとともに、このパターンをSwiftで実装する方法を整理します。
まずは要点
- Compositeパターン=LeafとCompositeを同じプロトコルでまとめる
- クライアントはファイルかフォルダーかを意識せず、同じように呼び出す
- フォルダーの計算は、子に再び尋ねる再帰構造
- Swiftでは
protocolだけで簡潔に実装できる
Compositeパターンとは?
文字どおり「合成」です。小さなものを集めて、1つの大きなまとまりにします。
最も身近な例はファイルシステムです。
ファイルは、それ自体で完結するLeafです。一方、フォルダーはファイルや別のフォルダーを含められる枝です。
しかし、ユーザーの視点は少し違います。フォルダーでもファイルでも、私たちは「開いて」と命令するだけです。
このように個々の要素と集合を同じ方法で扱えるようにするのが、Compositeパターンの目的です。
構成要素は3つに分かれます。
- Component:共通インターフェース(プロトコル)
- Leaf:子を持たない末端オブジェクト(ファイル)
- Composite:子を格納するオブジェクト(フォルダー)
Swiftでどう実装する?
まず共通プロトコルを定義します。名前とサイズを取得できるようにします。
以下は、ファイルとフォルダーが共通して従うルール(Component)です。
protocol FileComponent {
var name: String { get }
func size() -> Int // バイトサイズを返す
}
次はLeafにあたるファイルです。自分のサイズを返すだけなのでシンプルです。
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()が自分自身を再び呼び出す再帰構造だという点です。
フォルダーが何階層重なっていても、この1行が最下層まで自動的にたどります。
実際に使うとこんな感じ
それでは、ファイルとフォルダーを組み合わせてツリーを作ってみます。
let root = Folder(name: "ドキュメント", children: [
File(name: "メモ.txt", bytes: 100),
Folder(name: "写真", children: [
File(name: "旅行.jpg", bytes: 2000)
])
])
print(root.size()) // 2100 出力
わかりますか?root.size()を1回呼ぶだけで、フォルダー内の全ファイルのサイズが合計されます。
実際に動かして最も感心したのは、まさにここでした。
クライアントコードでは、「これはフォルダーかファイルか」と一度も尋ねません。
size()を呼ぶだけです。あとは各オブジェクトが自動的に処理します。
分岐文で処理する場合との違いは?
「if文で型を分けて処理すればよいのでは?」と思うかもしれません。
可能です。ただし、ツリーが深くなるほど差が大きくなります。
| 項目 | 分岐文(if/switch)方式 | Compositeパターン |
|---|---|---|
| 型の判定 | 毎回自分で確認 | 不要 |
| 新しい型の追加 | 各所の分岐文を修正 | プロトコルに準拠 |
| 再帰処理 | 自分で走査コードを書く | オブジェクトが自動処理 |
| コードの可読性 | 深くなるほど複雑 | 一定に保てる |
分岐方式では、新しい種類(例:ショートカットリンク)が増えるたび、あちこちにifを追加する必要があります。
一方Compositeなら、FileComponentに準拠する新しい型を1つ作るだけで済みます。既存コードを変更する必要はありません。
よくある質問
Q. LeafとCompositeのインターフェースは完全に同じである必要がありますか?
理想的にはそうです。ただし、ファイルに「子を追加」のような機能は不自然です。その場合は、共通動作(sizeなど)だけをプロトコルに置き、子の管理はフォルダーだけに任せるのが実用的です。
Q. classではなくstructを使ってもよいですか?
はい。上の例のように値型でも十分動作します。ただし、ツリーを頻繁に変更し、参照を共有する必要があるなら、classのほうが便利な場合があります。
ツリー構造を前にしてif地獄に陥っているなら、Compositeパターンを試してみてください。
ファイルシステムだけでなく、ビュー階層、メニュー、組織図など、「中にさらに中」がある構造ならどこでもよく適合します。
今日の例をそのままPlaygroundに貼って実行すれば、きっと感覚をつかめます。

