カナデKanade

12. コンポジットパターン (Composite)

1つのものと、いくつものまとまりを同じ約束で扱って、木の形の構造を一度に扱うコンポジットパターンについて学びましょう。

ひとことで言うと
1つのもの(ファイル)といくつもを入れたまとまり(フォルダ)を同じ約束に従わせて、使う側が2つを区別せずに木の形の構造全体を一度に扱えるようにするパターンです。

問題: 何かするたびに「ファイル?フォルダ?」と聞くコード

パソコンのフォルダ整理プログラムを作るとしましょう。フォルダの中にはファイルが入っていて、フォルダの中にさらにフォルダが入っていることもあります。

  • フォルダの全体のサイズを知りたいです。中のファイルと、中のフォルダに入ったファイルまで全部足さなければなりません。
  • フォルダの中身を木の形で見せたいです。

まず思いつくのは、ファイルとフォルダを別々に作り、何かするたびに「これはファイルなのかフォルダなのか」を確かめて分ける方法です。

【テキスト】から〈繰り返し〉を持ってこよう

【ファイル項目】を設計しよう:
    『名前』を【文字列】の「」にしよう
    『サイズ』を【数字】の0にしよう

    最初に作られる時(【文字列】の『名前』, 【数字】の『サイズ』)次のようにしよう:
        『私』の『名前』を『名前』にしよう
        『私』の『サイズ』を『サイズ』にしよう

【フォルダ】を設計しよう:
    『名前』を【文字列】の「」にしよう
    『中身』を【】にしよう

    最初に作られる時(【文字列】の『名前』)次のようにしよう:
        『私』の『名前』を『名前』にしよう

    〈入れる〉を作ろう(『もの』):
        『私』の『中身』に『もの』を追加しよう

(参考:何かするたびに「ファイル?フォルダ?」を確かめます)
〈サイズを測る〉を作ろう(『項目』):
    もし(『項目』が【ファイル項目】の一種だ)なら:
        『項目』の『サイズ』を返そう
    『合計』を0にしよう
    『項目』の『中身』の『ひとつ』ごとに繰り返そう:
        『合計』を(『合計』 + 〈サイズを測る〉(『ひとつ』))にしよう
    『合計』を返そう

〈見せる〉を作ろう(『項目』, 【数字】の『深さ』):
    『前』を〈繰り返し〉(「· 」, 『深さ』)にしよう
    もし(『項目』が【ファイル項目】の一種だ)なら:
        枠「{『前』}{『項目』の『名前』} ({『項目』の『サイズ』}KB)」を出力しよう
        返そう
    枠「{『前』}{『項目』の『名前』}/」を出力しよう
    『項目』の『中身』の『ひとつ』ごとに繰り返そう:
        〈見せる〉(『ひとつ』, 『深さ』 + 1)を実行しよう

『写真』を新しい【フォルダ】(「写真」)にしよう
『写真』の〈入れる〉(新しい【ファイル項目】(「海.jpg」, 300))を実行しよう
『写真』の〈入れる〉(新しい【ファイル項目】(「山.jpg」, 200))を実行しよう

『書類』を新しい【フォルダ】(「書類」)にしよう
『書類』の〈入れる〉(新しい【ファイル項目】(「日記.txt」, 5))を実行しよう
『書類』の〈入れる〉(『写真』)を実行しよう

〈見せる〉(『書類』, 0)を実行しよう
枠「全体のサイズ: {〈サイズを測る〉(『書類』)}KB」を出力しよう

実行結果
書類/
· 日記.txt (5KB)
· 写真/
· · 海.jpg (300KB)
· · 山.jpg (200KB)
全体のサイズ: 505KB

ちゃんと動きます。でも、やることが増えると痛くなります。

  • やることごとに「ファイル?フォルダ?」と聞いています。〈サイズを測る〉も〈見せる〉も一種だで分け、フォルダなら中を回りながら自分自身をまた呼んでいます。「名前で探す」「古いファイルを消す」を作るたびに、同じ形のコードをまた書かなければなりません。
  • 新しい種類ができると、すべての関数を直さなければなりません。「圧縮ファイル」や「ショートカット」ができたら、やることごとにもしを1つずつ足す必要があります。
  • 外側が中の事情を全部知っています。〈サイズを測る〉は、ファイルには『サイズ』が、フォルダには『中身』があることを知っています。フォルダが中身を入れるやり方を変えると、外側の関数も全部変わります。
  • 何でも入れられてしまいます。〈入れる〉(『もの』)には型がないので、うっかり数字や文字を入れても止められず、サイズを測る時になって初めておかしくなります。

解決策: 1つのものとまとまりを同じ約束に従わせる

コンポジットパターンは、ファイルとフォルダを同じ約束でまとめます。

  1. ファイルでもフォルダでも項目なら守る約束、つまり規定を作ります。項目は〈サイズ〉を教えられて、〈見せる〉ができなければなりません。
  2. 葉(ファイル)は自分の仕事を自分でします。サイズを聞かれたら、自分の容量を返します。
  3. まとまり(フォルダ)は項目たちを抱え、やることを中の項目たちに分けて任せます。 サイズを聞かれたら、中の項目たちにサイズを聞いて足します。中にフォルダがあっても、そのフォルダが自分で足すので、深さがいくつでも関係ありません。
  4. 使う側は、相手がファイルなのかフォルダなのかを聞きません。 ただ〈サイズ〉()を呼びます。

たとえ: 会社の連絡網

社長が「全員、明日は9時に集合」と伝える時、社員1人1人に直接言うわけではありません。部長に言えば部長は課長たちに、課長は係長たちに、係長はメンバーに伝えます。受け取る側が社員1人でも部署まるごとでも、社長は同じように「9時に集合」と言えばいいんです。部署(まとまり)は下へ伝え、社員(葉)は自分で動きます。この構造こそが「コンポジット」です。

だれが何をするの?

役割 この例では やること
項目の約束 【項目】(規定) 葉とまとまりが同じようにできなければならないことを決めます
葉 【ファイル項目】 それ以上分けられない1つです。仕事を自分でします
まとまり 【フォルダ】 項目たちを抱え、仕事を中の項目たちに分けて任せます
使う側 プログラムのいちばん下 ファイルかフォルダかを聞かず、約束どおりに呼びます

カナデで作る

同じフォルダ整理プログラムを、コンポジットパターンで作り直してみましょう。

【テキスト】から〈繰り返し〉を持ってこよう

(参考:ステップ1 — ファイルでもフォルダでも「項目」なら守る約束)
【項目】を規定しよう:
    【数字】を返す〈サイズ〉がなければならない()
    〈見せる〉がなければならない(【数字】の『深さ』)

(参考:ステップ2 — 葉:それ以上分けられない1つ)
【項目】に従う【ファイル項目】を設計しよう:
    『名前』を【文字列】の「」にしよう
    『容量』を【数字】の0にしよう

    最初に作られる時(【文字列】の『名前』, 【数字】の『容量』)次のようにしよう:
        『私』の『名前』を『名前』にしよう
        『私』の『容量』を『容量』にしよう

    【数字】を返す〈サイズ〉を作ろう():
        『私』の『容量』を返そう

    〈見せる〉を作ろう(【数字】の『深さ』):
        枠「{〈繰り返し〉(「· 」, 『深さ』)}{『私』の『名前』} ({『私』の『容量』}KB)」を出力しよう

(参考:ステップ3 — まとまり:項目たちを抱えて、やることを中の項目たちに分けて任せます)
【項目】に従う【フォルダ】を設計しよう:
    『名前』を【文字列】の「」にしよう
    『中身』を【】に隠そう

    最初に作られる時(【文字列】の『名前』)次のようにしよう:
        『私』の『名前』を『名前』にしよう

    〈入れる〉を作ろう(【項目】の『もの』):
        『私』の『中身』に『もの』を追加しよう

    【数字】を返す〈サイズ〉を作ろう():
        『合計』を0にしよう
        『私』の『中身』の『ひとつ』ごとに繰り返そう:
            『合計』を(『合計』 + 『ひとつ』の〈サイズ〉())にしよう
        『合計』を返そう

    〈見せる〉を作ろう(【数字】の『深さ』):
        枠「{〈繰り返し〉(「· 」, 『深さ』)}{『私』の『名前』}/」を出力しよう
        『私』の『中身』の『ひとつ』ごとに繰り返そう:
            『ひとつ』の〈見せる〉(『深さ』 + 1)を実行しよう

『写真』を新しい【フォルダ】(「写真」)にしよう
『写真』の〈入れる〉(新しい【ファイル項目】(「海.jpg」, 300))を実行しよう
『写真』の〈入れる〉(新しい【ファイル項目】(「山.jpg」, 200))を実行しよう

『書類』を新しい【フォルダ】(「書類」)にしよう
『書類』の〈入れる〉(新しい【ファイル項目】(「日記.txt」, 5))を実行しよう
『書類』の〈入れる〉(『写真』)を実行しよう

(参考:使う側は、ファイルかフォルダかを聞きません)
『書類』の〈見せる〉(0)を実行しよう
枠「全体のサイズ: {『書類』の〈サイズ〉()}KB」を出力しよう
枠「写真フォルダだけ: {『写真』の〈サイズ〉()}KB」を出力しよう

実行結果
書類/
· 日記.txt (5KB)
· 写真/
· · 海.jpg (300KB)
· · 山.jpg (200KB)
全体のサイズ: 505KB
写真フォルダだけ: 500KB

木の形と全体のサイズは、リファクタリング前と同じです。最後の行のように、内側のフォルダ1つだけを測るのも、同じ〈サイズ〉()でできます。

ステップごとにもう一度

  • ステップ1 — 同じ約束。【項目】は「サイズを教えられて、見せられれば項目」という約束です。ファイルもフォルダもこの約束に従います。
  • ステップ2 — 葉は自分で。【ファイル項目】の〈サイズ〉は、自分の『容量』を返すだけです。
  • ステップ3 — まとまりは分けて任せる。【フォルダ】の〈サイズ〉は、中の項目ごとに〈サイズ〉()を呼んで足します。中の項目がまたフォルダなら、そのフォルダがさらに自分の中の項目たちに聞きます。こうして木の先まで自然に降りていきます。深さを数えるコードも、一種だもありません。
  • 使う側は聞きません。『書類』の〈サイズ〉()も『写真』の〈サイズ〉()も、同じように呼びます。相手がファイル1つだったとしても、同じように呼べばいいんです。
  • 入れられるものも約束で。〈入れる〉は【項目】の『もの』だけを受け取るので、うっかり数字を入れるとその場で止められます。

「自分自身をまた呼ぶ」のは再帰では?
そのとおりです。前の〈サイズを測る〉も、後の【フォルダ】の〈サイズ〉も、再帰で木を降りていきます。違いはだれが再帰するかです。前では外側の関数がファイルかフォルダかを確かめながら再帰し、コンポジットではフォルダが自分の中の項目たちに聞くだけです。再帰がオブジェクトたちの間に自然に分かれて入り込むので、外側からは見えません。

新しい種類を足してみると

「圧縮ファイル」ができたとしましょう。サイズは元のサイズの半分として扱います。

  • 前のやり方:〈サイズを測る〉、〈見せる〉のように、やることの関数ごとにもし(『項目』が【圧縮ファイル】の一種だ)なら:を入れなければなりません。
  • コンポジットパターン:【項目】に従う【圧縮ファイル】を1つだけ作れば済みます。フォルダにそのまま入れられて、フォルダのサイズ計算にも自然に入ります。

やってみよう

  1. 上のコードに【圧縮ファイル】を足してみてください。名前と元の容量を受け取り、〈サイズ〉は容量の半分を返します。〈見せる〉は· · 写真まとめ.zip (圧縮, 100KB)のように見せましょう。
  2. 今度は新しいやることを足してみましょう。【項目】に【数字】を返す〈ファイル数〉がなければならない()を足し、ファイルは1を、フォルダは中の項目たちの〈ファイル数〉を足して返すようにしてみてください。

どんな時に使うといいの?

  • データが木の形の時(フォルダとファイル、メニューとサブメニュー、会社の組織図、画面の箱の中の箱)
  • 1つのものとまとまりを同じように扱いたい時
  • 深さがどれくらいになるか分からない時
  • 「ファイル?フォルダ?」と聞くもしが、いくつもの関数でくり返される時

いいところと気になるところ

いいところ 気になるところ
使う側が葉とまとまりを区別しなくて済みます 約束が広くなりすぎると、葉に似合わない仕事(ファイルに「入れる」?)まで約束に入りやすくなります
深さに関係なく、木全体を一度に扱えます まとまりの中に特定の種類だけが入るように止めるのは難しいです
新しい種類を足しても、使う側はそのままです とても大きな木では、毎回先まで降りていく計算が遅くなることがあります
再帰がオブジェクトの中に隠れて、コードが単純になります

早すぎるパターンは毒です。
フォルダの中にフォルダが入ることがなく、深さがいつも1段階なら、リストを1つ回るだけで十分です。まとまりの中にまとまりが入り始めた時が、コンポジットを使う時です。

ほかのパターンとの関係

  • デコレーターパターンと似ています。 どちらも同じ約束に従うオブジェクトを抱えます。デコレーターは1つを包んで機能を足し、コンポジットはいくつもを入れて仕事を分けて任せます。
  • イテレーターパターンと一緒に使うと便利です。 木のすべての項目を1つずつ順番に取り出して見たい時、その順番をイテレーターに任せられます。
  • コマンドパターンで「まとめコマンド」が作れます。 いくつものコマンドを入れたまとめコマンドもコマンドの約束に従わせれば、一度に実行し、一度に元に戻せます。まさにコンポジットの形です。
プレイグラウンド
結果
コードを書いて「実行」ボタンを押してください。