カナデKanade

10. ファサードパターン (Facade)

複雑な内部システムを隠して単純化された単一の窓口を提供するファサードパターンについて学びましょう。

ひとことで言うと
いくつものオブジェクトを決まった順番で扱わなければならない複雑な仕事を窓口1つの後ろにまとめ、使う側はその窓口に1回頼むだけにするパターンです。ファサード(Facade)は建物の「正面」という意味です。

問題: 使う場所ごとに複雑な順番を覚えなければならないコード

ネットショップの注文プログラムを作るとしましょう。注文を1つ受けるには、4つのシステムを順番どおりに扱わなければなりません。

  1. 在庫が足りるか確かめ、あれば減らします。
  2. 決済機でお金を受け取ります。(1個1000円)
  3. 配送を受け付けます。
  4. お客さんにSMSを送ります。

注文はアプリからも、お店の前のキオスクからも入ってきます。まず思いつくのは、注文を受ける場所ごとに4つのシステムを直接扱う方法です。

(参考:注文1つに必要なシステムです。それぞれできることが違います)
【在庫】を設計しよう:
    『残り』を【数字】の2にしよう

    【論理】を返す〈あるか〉を作ろう(【数字】の『個数』):
        (『私』の『残り』 >= 『個数』)を返そう

    〈減らす〉を作ろう(【数字】の『個数』):
        『私』の『残り』を(『私』の『残り』 - 『個数』)にしよう

【決済機】を設計しよう:
    〈決済する〉を作ろう(【数字】の『円』):
        枠「[決済] {『円』}円」を出力しよう

【配送】を設計しよう:
    〈受け付ける〉を作ろう(【文字列】の『住所』):
        枠「[配送] {『住所』}へ受付」を出力しよう

【SMS】を設計しよう:
    〈送る〉を作ろう(【文字列】の『内容』):
        枠「[SMS] {『内容』}」を出力しよう

(参考:注文を受ける場所ごとに、4つのシステムを直接、順番どおりに扱います)
〈アプリ注文〉を作ろう(『在庫』, 『決済機』, 『配送』, 『SMS』, 【数字】の『個数』, 【文字列】の『住所』):
    もし(『在庫』の〈あるか〉(『個数』)が偽と同じだ)なら:
        「[アプリ] 在庫が足りません。」を出力しよう
        返そう
    『在庫』の〈減らす〉(『個数』)を実行しよう
    『決済機』の〈決済する〉(『個数』 * 1000)を実行しよう
    『配送』の〈受け付ける〉(『住所』)を実行しよう
    『SMS』の〈送る〉(「ご注文を受け付けました!」)を実行しよう

〈キオスク注文〉を作ろう(『在庫』, 『決済機』, 『配送』, 『SMS』, 【数字】の『個数』, 【文字列】の『住所』):
    (参考:在庫の確認をうっかり忘れました)
    『在庫』の〈減らす〉(『個数』)を実行しよう
    『決済機』の〈決済する〉(『個数』 * 1000)を実行しよう
    『配送』の〈受け付ける〉(『住所』)を実行しよう
    『SMS』の〈送る〉(「ご注文を受け付けました!」)を実行しよう

『お店の在庫』を新しい【在庫】()にしよう
『お店の決済機』を新しい【決済機】()にしよう
『お店の配送』を新しい【配送】()にしよう
『お店のSMS』を新しい【SMS】()にしよう

〈アプリ注文〉(『お店の在庫』, 『お店の決済機』, 『お店の配送』, 『お店のSMS』, 2, 「東京都カナデ区」)を実行しよう
〈キオスク注文〉(『お店の在庫』, 『お店の決済機』, 『お店の配送』, 『お店のSMS』, 1, 「大阪府ハリ市」)を実行しよう
枠「残りの在庫: {『お店の在庫』の『残り』}」を出力しよう

実行結果
[決済] 2000円
[配送] 東京都カナデ区へ受付
[SMS] ご注文を受け付けました!
[決済] 1000円
[配送] 大阪府ハリ市へ受付
[SMS] ご注文を受け付けました!
残りの在庫: -1

在庫が「-1」になりました。ない品物を売って、お金まで受け取ってしまいました。

  • 同じ順番が、注文を受ける場所ごとにコピーされます。〈アプリ注文〉と〈キオスク注文〉が、4つのシステムをまったく同じ順番で扱っています。電話注文ができたら、またコピーしなければなりません。
  • 1か所で忘れるとバグになります。 キオスク側は在庫の確認をうっかり忘れました。エラーにはなりませんでしたが、在庫がマイナスになりました。
  • 注文を受ける場所が知りすぎています。〈キオスク注文〉は「注文を受ける」ことだけを知っていればいいのに、4つのシステムの名前とメソッド、順番、値段の計算まで全部知っていなければなりません。引数だけで6つです。
  • 順番が変わると、すべての場所を直さなければなりません。「SMSを決済の前に送ってください」と頼まれたら、注文を受ける場所を1つずつ探して直す必要があります。

解決策: 複雑な仕事の前に窓口を1つ立てる

ファサードパターンは、複雑なシステムたちを直さずに、その前にかんたんな窓口を1つ立てます。

  1. 在庫、決済機、配送、SMSはそのままにします。それぞれ自分の仕事をきちんとすればいいんです。
  2. 窓口を作ります。窓口は4つのシステムを抱えています。
  3. 窓口に〈注文する〉を1つ作り、その中に正しい順番を1回だけ書きます。在庫の確認もここにあります。
  4. 注文を受ける場所は、もう窓口に〈注文する〉を1回頼むだけです。4つのシステムは知らなくてかまいません。

たとえ: ホテルのフロント

ホテルで「明日の朝7時に起こしてください。それとタクシーを1台呼んでください」とフロントに言えば、それで終わりです。フロントが、モーニングコールの係、タクシー会社、清掃の係に自分で連絡してくれます。お客さんは、その人たちの電話番号も、頼む順番も知らなくてかまいません。フロントこそが「ファサード」です。だからといって係の人たちがいなくなったわけではありません。必要なら、お客さんが清掃の係に直接頼むこともできます。

だれが何をするの?

役割 この例では やること
複雑なシステムたち 【在庫】、【決済機】、【配送】、【SMS】 それぞれ自分の仕事をします。ファサードのことは知りません
ファサード(窓口) 【注文窓口】 システムたちを抱え、正しい順番で扱います
使う場所 〈アプリ注文〉、〈キオスク注文〉 窓口に1回頼むだけです

カナデで作る

同じお店をファサードパターンで作り直してみましょう。4つのシステムのコードは1文字も変わっていません。

(参考:注文1つに必要なシステムです。それぞれできることが違います)
【在庫】を設計しよう:
    『残り』を【数字】の2にしよう

    【論理】を返す〈あるか〉を作ろう(【数字】の『個数』):
        (『私』の『残り』 >= 『個数』)を返そう

    〈減らす〉を作ろう(【数字】の『個数』):
        『私』の『残り』を(『私』の『残り』 - 『個数』)にしよう

【決済機】を設計しよう:
    〈決済する〉を作ろう(【数字】の『円』):
        枠「[決済] {『円』}円」を出力しよう

【配送】を設計しよう:
    〈受け付ける〉を作ろう(【文字列】の『住所』):
        枠「[配送] {『住所』}へ受付」を出力しよう

【SMS】を設計しよう:
    〈送る〉を作ろう(【文字列】の『内容』):
        枠「[SMS] {『内容』}」を出力しよう

(参考:4つのシステムを抱えて、注文1回に必要な順番を1か所で守る窓口です)
【注文窓口】を設計しよう:
    『在庫』を【在庫】の新しい【在庫】()に隠そう
    『決済機』を【決済機】の新しい【決済機】()に隠そう
    『配送』を【配送】の新しい【配送】()に隠そう
    『SMS』を【SMS】の新しい【SMS】()に隠そう

    【論理】を返す〈注文する〉を作ろう(【数字】の『個数』, 【文字列】の『住所』):
        もし(『私』の『在庫』の〈あるか〉(『個数』)が偽と同じだ)なら:
            「在庫が足りません。」を出力しよう
            偽を返そう
        『私』の『在庫』の〈減らす〉(『個数』)を実行しよう
        『私』の『決済機』の〈決済する〉(『個数』 * 1000)を実行しよう
        『私』の『配送』の〈受け付ける〉(『住所』)を実行しよう
        『私』の『SMS』の〈送る〉(「ご注文を受け付けました!」)を実行しよう
        真を返そう

    【数字】を返す〈残りの在庫〉を作ろう():
        『私』の『在庫』の『残り』を返そう

(参考:注文を受ける場所は、窓口1つだけを知っています)
〈アプリ注文〉を作ろう(【注文窓口】の『窓口』, 【数字】の『個数』, 【文字列】の『住所』):
    『窓口』の〈注文する〉(『個数』, 『住所』)を実行しよう

〈キオスク注文〉を作ろう(【注文窓口】の『窓口』, 【数字】の『個数』, 【文字列】の『住所』):
    『窓口』の〈注文する〉(『個数』, 『住所』)を実行しよう

『お店』を新しい【注文窓口】()にしよう
〈アプリ注文〉(『お店』, 2, 「東京都カナデ区」)を実行しよう
〈キオスク注文〉(『お店』, 1, 「大阪府ハリ市」)を実行しよう
枠「残りの在庫: {『お店』の〈残りの在庫〉()}」を出力しよう

実行結果
[決済] 2000円
[配送] 東京都カナデ区へ受付
[SMS] ご注文を受け付けました!
在庫が足りません。
残りの在庫: 0

今度は、キオスクの注文が「在庫が足りません」で止められ、在庫は0で止まりました。キオスク側で在庫の確認を忘れることはできません。確かめるコードが窓口の中に1回だけあるからです。

1つずつもう一度

  • システムたちはそのまま。【在庫】、【決済機】、【配送】、【SMS】は変わっていません。ファサードがあることも知りません。
  • 順番は窓口1か所に。 在庫の確認 → 在庫を減らす → 決済 → 配送 → SMS の順番が、〈注文する〉の中にだけあります。「SMSを決済の前に」のような頼みが来たら、ここの2行を入れ替えるだけで済みます。
  • 注文を受ける場所は1行。〈アプリ注文〉と〈キオスク注文〉は、窓口に〈注文する〉を頼むだけです。引数も6つから3つに減りました。
  • 結果も教えてくれます。〈注文する〉は、注文できたかどうかを真/偽で返します。注文を受ける場所は、必要ならこの値だけを見て画面に知らせればいいんです。

窓口はシステムを完全に隠すの?
必ずしもそうではありません。この例ではシステムたちを隠そうで入れて、窓口を通してしか使えないようにしましたが、ファサードの要は「よくある複雑な仕事をかんたんに」することです。まれにしか使わない細かい仕事(在庫だけを補充する、など)は、システムを直接使えるように開けておくことも多いです。この例の〈残りの在庫〉のように、必要なものだけを窓口に1つずつ足してもかまいません。

注文を受ける場所を足してみると

「電話注文」ができたとしましょう。

  • 前のやり方: 4つのシステムを扱う順番を、また1セットコピーしなければなりません。今回も何かを忘れるかもしれません。
  • ファサードパターン:〈電話注文〉を作り、その中で『窓口』の〈注文する〉(...)を1回呼べば終わりです。順番を忘れようがありません。

やってみよう

  1. 上のコードに〈電話注文〉を足して、在庫が0の今の状態で1個注文してみてください。「在庫が足りません。」が出るはずです。
  2. 【注文窓口】に〈入荷する〉(【数字】の『個数』)を足して、在庫を補充できるようにしてみましょう。【在庫】にはもう〈減らす〉があるので、同じように〈増やす〉を作ればいいんです。補充してから、もう一度注文してみてください。

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

  • 1つの仕事をするのに、いくつものオブジェクトを決まった順番で扱わなければならない時
  • その順番があちこちにコピーされ始めた時
  • 使う側が複雑な中身を知らなくて済むようにしたい時(初めて使う人、ほかのチーム、パッケージを使う人)
  • 複雑なシステムを変えたり差し替えたりする時に、変わる場所を窓口1か所にまとめたい時

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

いいところ 気になるところ
複雑な順番が1か所にしかないので、忘れようがありません 窓口があれこれ引き受けていると、大きくなりすぎることがあります
使う側のコードが短く、かんたんになります 窓口にない細かい仕事は、結局システムを直接使う必要があります
システムが変わっても、窓口だけ直せば済みます すべての道が窓口1つに集まるので、窓口を直す時に気を配る場所が多くなります
システムたちは1つも直しません

早すぎるパターンは毒です。
注文を受ける場所が1か所だけで、手順も2〜3個なら、その場で直接呼ぶほうが読みやすいです。同じ順番を2回目にコピーしようとした瞬間、あるいは「どこかで1つ手順を忘れていた」というバグが初めて出た瞬間が、窓口を立てる時です。

ほかのパターンとの関係

  • アダプターパターンと比べてみましょう。 アダプターはオブジェクト1つの形をうちの約束に合わせて変え、ファサードはいくつものオブジェクトをまとめて、もっとかんたんな窓口を新しく作ります。
  • シングルトンパターンにすることもあります。 お店の注文窓口はふつう1つあれば十分なので、シングルトンにして、アプリ、キオスク、電話がみんな同じ窓口を使うようにできます。
  • ビルダーパターンと一緒に使えます。 窓口の中で複雑な注文書をビルダーで組み立てれば、使う側はやはり〈注文する〉1行だけを知っていれば済みます。
プレイグラウンド
結果
コードを書いて「実行」ボタンを押してください。