カナデKanade

7. デコレーターパターン (Decorator)

オブジェクトに殻を被せるように動的に機能を追加するデコレーターパターンについて学びましょう。

ひとことで言うと
オブジェクトを同じ約束に従う殻で包んで機能を付け足すパターンです。殻の上にさらに殻をかぶせられるので、機能を好きなだけ、好きな順番で重ねられます。

問題: 組み合わせごとに1つずつ増えるクラス

カフェの会計プログラムを作るとしましょう。基本はエスプレッソで、お客さんがトッピングを選びます。

  • ミルク +500円
  • シロップ +300円
  • ショット +500円

まず思いつくのは、メニューごとに継承でクラスを1つずつ作る方法です。

【エスプレッソ】を設計しよう:
    【数字】を返す〈値段〉を作ろう():
        3000を返そう

    【文字列】を返す〈説明〉を作ろう():
        「エスプレッソ」を返そう

(参考:組み合わせごとにクラスを1つずつ作ります)
【エスプレッソ】をもとにして【ラテ】を設計しよう:
    【数字】を返す〈値段〉を作ろう():
        (3000 + 500)を返そう

    【文字列】を返す〈説明〉を作ろう():
        「エスプレッソ + ミルク」を返そう

【エスプレッソ】をもとにして【バニララテ】を設計しよう:
    【数字】を返す〈値段〉を作ろう():
        (3000 + 500 + 300)を返そう

    【文字列】を返す〈説明〉を作ろう():
        「エスプレッソ + ミルク + シロップ」を返そう

(参考:ミルクなしでシロップだけ入れるには、もう1つ作らなければなりません)
【エスプレッソ】をもとにして【バニラエスプレッソ】を設計しよう:
    【数字】を返す〈値段〉を作ろう():
        (3000 + 300)を返そう

    【文字列】を返す〈説明〉を作ろう():
        「エスプレッソ + シロップ」を返そう

『メニュー』を【新しい【エスプレッソ】(), 新しい【ラテ】(), 新しい【バニララテ】(), 新しい【バニラエスプレッソ】()】にしよう
『メニュー』の『コーヒー』ごとに繰り返そう:
    枠「{『コーヒー』の〈説明〉()}: {『コーヒー』の〈値段〉()}円」を出力しよう

実行結果
エスプレッソ: 3000円
エスプレッソ + ミルク: 3500円
エスプレッソ + ミルク + シロップ: 3800円
エスプレッソ + シロップ: 3300円

メニューが4つのうちは大丈夫です。でも、トッピングで選べるものが増えると、すぐに痛くなります。

  • 組み合わせの数だけクラスが増えます。 トッピングが3種類なら入れる・入れないの組み合わせは8通り、4種類なら16通りです。トッピングが1つ増えるたびに、クラスの数が2倍になります。
  • 同じ計算があちこちにコピーされます。「シロップは+300円」が【バニララテ】と【バニラエスプレッソ】に別々に入っています。シロップが400円に値上がりしたら、シロップの入ったクラスを全部探して直さなければなりません。
  • 同じトッピングを2回入れるのもクラスです。「ショット2回」にするには、【ダブルショットエスプレッソ】をまた作る必要があります。
  • お客さんの注文は実行中に入ってきます。 でもクラスはコードを書く時に決まっています。だれも予想しなかった組み合わせ(ショット + ミルク + シロップ2回)を注文されると、受け止めるクラスがありません。

解決策: 同じ形の殻で包む

デコレーターパターンは、トッピングを「コーヒーの種類」ではなく「コーヒーを包む殻」と考えます。

  1. コーヒーなら守る約束、つまり規定を作ります。〈値段〉と〈説明〉ができなければなりません。
  2. 殻にもこの約束を守らせます。殻は中にコーヒーを1つ包んでいて、ふだんは中身のコーヒーに仕事をそのまま渡します。
  3. トッピングごとに殻を1つずつ作り、「中身の結果に何を足すか」だけを書きます。ミルクの殻は、中身の値段に500を足します。
  4. 殻もコーヒーなので、殻をさらに別の殻で包めます。 順番も数も自由です。

たとえ: プレゼントのラッピング

プレゼントを箱に入れて、包装紙で包んで、リボンを付けます。包装紙で包んだ箱も「プレゼント」のままなので、その上にさらにリボンを付けられます。受け取った人は外側から1枚ずつほどいて、中のプレゼントにたどり着きます。ラッピングの1枚1枚が「デコレーター」です。包装紙の種類が増えても、「ラッピング済みの箱」の種類を前もって全部作っておく必要はありませんよね。

だれが何をするの?

役割 この例では やること
約束 【コーヒー】(規定) コーヒーでも殻でも、できなければならないことを決めます
中身 【エスプレッソ】 包まれる基本のオブジェクトです
基本の殻 【トッピング】 中身のコーヒーを包み、ふだんは仕事をそのまま渡します
具体的な殻 【ミルク】、【シロップ】、【ショット】 中身の結果に自分の分を足します

カナデで作る

同じカフェをデコレーターパターンで作り直してみましょう。コードの中の(参考:ステップn)に沿って読んでみてください。

(参考:ステップ1 — コーヒーなら守る約束)
【コーヒー】を規定しよう:
    【数字】を返す〈値段〉がなければならない()
    【文字列】を返す〈説明〉がなければならない()

【コーヒー】に従う【エスプレッソ】を設計しよう:
    【数字】を返す〈値段〉を作ろう():
        3000を返そう

    【文字列】を返す〈説明〉を作ろう():
        「エスプレッソ」を返そう

(参考:ステップ2 — コーヒーを1つ包んでいる殻。殻もコーヒーなので、ふだんは中身にそのまま任せます)
【コーヒー】に従う【トッピング】を設計しよう:
    『中身』を【コーヒー】の空っぽに譲ろう

    最初に作られる時(【コーヒー】の『中身』)次のようにしよう:
        『私』の『中身』を『中身』にしよう

    【数字】を返す〈値段〉を作ろう():
        『私』の『中身』の〈値段〉()を返そう

    【文字列】を返す〈説明〉を作ろう():
        『私』の『中身』の〈説明〉()を返そう

(参考:ステップ3 — 殻ごとに「中身に何を足すか」だけを書きます)
【トッピング】をもとにして【ミルク】を設計しよう:
    【数字】を返す〈値段〉を作ろう():
        (『私』の『中身』の〈値段〉() + 500)を返そう

    【文字列】を返す〈説明〉を作ろう():
        枠「{『私』の『中身』の〈説明〉()} + ミルク」を返そう

【トッピング】をもとにして【シロップ】を設計しよう:
    【数字】を返す〈値段〉を作ろう():
        (『私』の『中身』の〈値段〉() + 300)を返そう

    【文字列】を返す〈説明〉を作ろう():
        枠「{『私』の『中身』の〈説明〉()} + シロップ」を返そう

【トッピング】をもとにして【ショット】を設計しよう:
    【数字】を返す〈値段〉を作ろう():
        (『私』の『中身』の〈値段〉() + 500)を返そう

    【文字列】を返す〈説明〉を作ろう():
        枠「{『私』の『中身』の〈説明〉()} + ショット」を返そう

(参考:殻を好きなだけ、好きな順番でかぶせます)
『バニララテ』を【コーヒー】の新しい【シロップ】(新しい【ミルク】(新しい【エスプレッソ】()))にしよう
『濃いコーヒー』を【コーヒー】の新しい【ショット】(新しい【ショット】(新しい【エスプレッソ】()))にしよう

『メニュー』を【新しい【エスプレッソ】(), 『バニララテ』, 『濃いコーヒー』】にしよう
『メニュー』の『コーヒー』ごとに繰り返そう:
    枠「{『コーヒー』の〈説明〉()}: {『コーヒー』の〈値段〉()}円」を出力しよう

(参考:お客さんが選んだ順番どおりに、実行中に殻をかぶせることもできます)
『注文』を【「ショット」, 「ミルク」, 「シロップ」, 「シロップ」】にしよう
『お客さんのコーヒー』を【コーヒー】の新しい【エスプレッソ】()にしよう
『注文』の『名前』ごとに繰り返そう:
    『名前』によって分けよう:
        「ミルク」の場合:
            『お客さんのコーヒー』を新しい【ミルク】(『お客さんのコーヒー』)にしよう
        「シロップ」の場合:
            『お客さんのコーヒー』を新しい【シロップ】(『お客さんのコーヒー』)にしよう
        「ショット」の場合:
            『お客さんのコーヒー』を新しい【ショット】(『お客さんのコーヒー』)にしよう
枠「お客さんの注文 — {『お客さんのコーヒー』の〈説明〉()}: {『お客さんのコーヒー』の〈値段〉()}円」を出力しよう

実行結果
エスプレッソ: 3000円
エスプレッソ + ミルク + シロップ: 3800円
エスプレッソ + ショット + ショット: 4000円
お客さんの注文 — エスプレッソ + ショット + ミルク + シロップ + シロップ: 4600円

トッピングのクラスは3つだけなのに、どんな組み合わせでも作れます。最後の行のように、だれも前もって作っておかなかった組み合わせもです。

ステップごとにもう一度

  • ステップ1 — 約束を作る。【コーヒー】は「値段と説明を教えられればコーヒー」という約束です。中身も殻もこの約束に従うので、使う側は相手が中身なのか殻なのかを気にしません。
  • ステップ2 — 基本の殻。【トッピング】は中身のコーヒーを『中身』に包み、ふだんは仕事をそのまま渡します。『中身』は譲ろうで作ったので、外からは触れず、子の殻だけが使えます。
  • ステップ3 — 殻ごとに自分の分だけ。【ミルク】は「中身の値段 + 500、中身の説明 + ミルク」だけを知っています。作られる時の処理は書いていませんが、子にコンストラクタがなければ、親(【トッピング】)のコンストラクタが代わりに使われるからです。
  • 包む順番がそのまま結果の順番。新しい【シロップ】(新しい【ミルク】(...))は、まずミルクで包み、さらにシロップで包みます。だから説明も「ミルク + シロップ」の順に出ます。

〈値段〉を呼ぶと何が起こるの?
『バニララテ』の〈値段〉()を呼ぶと、いちばん外側の【シロップ】がまず中身の【ミルク】に聞き、【ミルク】はさらに【エスプレッソ】に聞きます。【エスプレッソ】が3000を返すと、【ミルク】が500を足して3500、【シロップ】が300を足して3800になります。ラッピングをほどくように外から中へ入っていき、結果は中から外へ積み上がって出てきます。

新しいトッピングを足してみると

「ホイップクリーム」+700円ができたとしましょう。

  • 前のやり方: ホイップクリームが入りうるすべての組み合わせごとに、クラスを新しく作らなければなりません。今あるクラスの数だけ、さらに増えます。
  • デコレーターパターン:【トッピング】をもとにして【ホイップ】という殻を1つ作るだけです。すでにあるどの組み合わせの上にも、すぐに乗せられます。

やってみよう
上のコードに【ホイップ】の殻を足してみてください。値段は中身 + 700円、説明は+ ホイップです。それから新しい【ホイップ】(新しい【ミルク】(新しい【エスプレッソ】()))の説明と値段を出力してみましょう。ほかのクラスは1つも直さなくていいはずです。

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

  • 機能を選んで組み合わせる必要があり、組み合わせが多すぎてクラスで全部作れない時
  • 同じ機能を何度も重ねて使ったり、順番を入れ替えて使ったりしたい時
  • すでにあるクラスを直さずに機能を付け足したい時(ほかの人が作ったクラス、パッケージのクラス)
  • 組み合わせが実行中に決まる時(ユーザーの選択、設定)

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

いいところ 気になるところ
トッピングの数だけクラスを作れば、すべての組み合わせができます 小さなオブジェクトが何重にもなって、ひと目で見渡しにくくなります
同じ計算が1か所(その殻)にしかありません 包む順番が結果に影響するので、順番に気を付けなければならない時があります
実行中に機能を付けたり外したりできます 途中の殻を1枚だけ抜き取るのは難しいです
元のクラスを直しません

早すぎるパターンは毒です。
メニューが決まっていて組み合わせが2〜3個だけなら、メニューごとにクラスを置くか、値1つで扱うほうが読みやすいです。組み合わせが増えてクラスがふくらみ始めてから殻に変えても、遅くはありません。

ほかのパターンとの関係

  • ストラテジーパターンと比べてみましょう。 ストラテジーは仕事の中身(方法)を差し替え、デコレーターは仕事を外側から包んで機能を足します。ストラテジーはオブジェクトの中に1つだけ差し込みますが、デコレーターは何重にも重ねます。
  • アダプターパターンも包む形です。 ただしアダプターは形(約束)を変えるために包み、デコレーターは形はそのままで機能を足すために包みます。
  • ファクトリーパターンと一緒に使えます。 上の例の最後のように、注文の名前を見てどの殻をかぶせるか選ぶコードを工場に移せば、注文を受ける場所はもっと簡単になります。
プレイグラウンド
結果
コードを書いて「実行」ボタンを押してください。