カナデKanade

13. プロキシパターン (Proxy)

本物のオブジェクトの前に同じ約束に従う代理人を立てて、呼ぶ側に知られずにアクセスを調整するプロキシパターンについて学びましょう。

ひとことで言うと
本物のオブジェクトの前に、同じ約束に従う代理人を立てて、呼ぶ側はそのままに本物へ行く道を調整するパターンです。覚えておく、止める、後から作る、記録を残す、といった仕事を代理人が引き受けます。

問題: 同じことを何度も聞き直すコード

天気アプリを作るとしましょう。天気は遠くにあるサーバーに聞かないと分かりません。サーバーは1回聞くたびに遅く、お金もかかります。

  • トップ画面は東京の天気を見せます。
  • 地図画面は東京と大阪の天気を見せます。

まず思いつくのは、画面たちがサーバーに直接聞く方法です。

【天気情報】を規定しよう:
    【文字列】を返す〈今日の天気〉がなければならない(【文字列】の『都市』)

(参考:遠くのサーバーに聞くサービスです。1回聞くたびに遅く、お金もかかります)
【天気情報】に従う【天気サーバー】を設計しよう:
    『聞いた回数』を【数字】の0にしよう

    【文字列】を返す〈今日の天気〉を作ろう(【文字列】の『都市』):
        『私』の『聞いた回数』を(『私』の『聞いた回数』 + 1)にしよう
        枠「(サーバーに{『都市』}の天気を聞いています...)」を出力しよう
        『都市』によって分けよう:
            「東京」の場合:
                「晴れ」を返そう
            「大阪」の場合:
                「雨」を返そう
        「不明」を返そう

〈トップ画面〉を作ろう(【天気情報】の『天気』):
    枠「[トップ] 東京: {『天気』の〈今日の天気〉(「東京」)}」を出力しよう

〈地図画面〉を作ろう(【天気情報】の『天気』):
    枠「[地図] 東京: {『天気』の〈今日の天気〉(「東京」)}、大阪: {『天気』の〈今日の天気〉(「大阪」)}」を出力しよう

『サーバー』を新しい【天気サーバー】()にしよう
〈トップ画面〉(『サーバー』)を実行しよう
〈地図画面〉(『サーバー』)を実行しよう
〈トップ画面〉(『サーバー』)を実行しよう
枠「サーバーに聞いた回数: {『サーバー』の『聞いた回数』}」を出力しよう

実行結果
(サーバーに東京の天気を聞いています…)
[トップ] 東京: 晴れ
(サーバーに東京の天気を聞いています…)
(サーバーに大阪の天気を聞いています…)
[地図] 東京: 晴れ、大阪: 雨
(サーバーに東京の天気を聞いています…)
[トップ] 東京: 晴れ
サーバーに聞いた回数: 4

画面は3回描いたのに、サーバーには4回も聞いています。そのうち3回は同じ「東京」でした。

  • 同じ質問を何度もし直しています。 今日の東京の天気は何回聞いても同じなのに、画面を描くたびにサーバーに聞き直します。遅く、お金もかかります。
  • 画面ごとに覚えておく仕組みを入れるとコピーになります。〈トップ画面〉と〈地図画面〉がそれぞれ「もう聞いたなら聞き直さない」を入れると、同じコードが2セットになり、記憶も別々なので分け合うこともできません。
  • サーバーは直せません。 サーバーはよそが作ったものかもしれませんし、サーバーに覚えておく仕事まで入れると、「天気を教える」という本来の仕事と混ざります。

解決策: 同じ約束に従う代理人を立てる

プロキシパターンは、サーバーの前に代理人(プロキシ)を1人立てます。

  1. 本物のサーバーが従う約束(【天気情報】)があります。画面たちはこの約束だけを知っています。
  2. プロキシも同じ約束に従います。だから画面から見ると、プロキシと本物のサーバーは同じに見えます。
  3. プロキシは中に本物のサーバーを抱え、頼まれるとまず自分で判断します。 もう聞いた都市なら覚えておいた答えをすぐに返し、初めての都市だけを本物のサーバーに聞きます。
  4. 画面たちは、本物のサーバーの代わりにプロキシを受け取ります。画面のコードは1文字も変わりません。

たとえ: 芸能人のマネージャー

テレビ局が芸能人に出演の依頼をすると、マネージャーが電話に出ます。マネージャーも「芸能人側の連絡先」なので、かける側は同じように電話をかけるだけです。マネージャーは、もう答えた質問(「来週のスケジュール空いていますか?」)にはすぐ答え、怪しい連絡は止め、本当に必要なことだけを芸能人に伝えます。マネージャーこそが「プロキシ」です。

だれが何をするの?

役割 この例では やること
約束 【天気情報】(規定) 本物も代理人も従わなければならない形です
本物のオブジェクト 【天気サーバー】 実際に仕事をします。遅くて高いです
プロキシ(代理人) 【天気キャッシュ】 同じ約束に従い、本物へ行く道を調整します
使う側 〈トップ画面〉、〈地図画面〉 約束だけを知っています。プロキシか本物かは知りません

カナデで作る

同じ天気アプリにプロキシを立ててみましょう。サーバーと画面のコードはそのままです。

(参考:ステップ1 — 天気を教える側なら守る約束。プロキシもこの約束に従います)
【天気情報】を規定しよう:
    【文字列】を返す〈今日の天気〉がなければならない(【文字列】の『都市』)

(参考:遠くのサーバーに聞くサービスです。1回聞くたびに遅く、お金もかかります)
【天気情報】に従う【天気サーバー】を設計しよう:
    『聞いた回数』を【数字】の0にしよう

    【文字列】を返す〈今日の天気〉を作ろう(【文字列】の『都市』):
        『私』の『聞いた回数』を(『私』の『聞いた回数』 + 1)にしよう
        枠「(サーバーに{『都市』}の天気を聞いています...)」を出力しよう
        『都市』によって分けよう:
            「東京」の場合:
                「晴れ」を返そう
            「大阪」の場合:
                「雨」を返そう
        「不明」を返そう

(参考:ステップ2 — 本物のサーバーの前に立って、同じ約束で代わりに答えるプロキシです)
【天気情報】に従う【天気キャッシュ】を設計しよう:
    『サーバー』を【天気情報】の空っぽに隠そう
    『記憶』を{}に隠そう

    最初に作られる時(【天気情報】の『サーバー』)次のようにしよう:
        『私』の『サーバー』を『サーバー』にしよう

    【文字列】を返す〈今日の天気〉を作ろう(【文字列】の『都市』):
        (参考:ステップ3 — もう聞いた都市なら、サーバーに行かずに覚えておいた答えを返します)
        もし(『私』の『記憶』の〈含むか確認〉(『都市』))なら:
            (『私』の『記憶』の『都市』)を返そう
        『答え』を『私』の『サーバー』の〈今日の天気〉(『都市』)にしよう
        『私』の『記憶』の『都市』を『答え』にしよう
        『答え』を返そう

(参考:画面のコードは1文字も変わりません。【天気情報】だけを知っているからです)
〈トップ画面〉を作ろう(【天気情報】の『天気』):
    枠「[トップ] 東京: {『天気』の〈今日の天気〉(「東京」)}」を出力しよう

〈地図画面〉を作ろう(【天気情報】の『天気』):
    枠「[地図] 東京: {『天気』の〈今日の天気〉(「東京」)}、大阪: {『天気』の〈今日の天気〉(「大阪」)}」を出力しよう

『サーバー』を新しい【天気サーバー】()にしよう
『天気』を【天気情報】の新しい【天気キャッシュ】(『サーバー』)にしよう
〈トップ画面〉(『天気』)を実行しよう
〈地図画面〉(『天気』)を実行しよう
〈トップ画面〉(『天気』)を実行しよう
枠「サーバーに聞いた回数: {『サーバー』の『聞いた回数』}」を出力しよう

実行結果
(サーバーに東京の天気を聞いています…)
[トップ] 東京: 晴れ
(サーバーに大阪の天気を聞いています…)
[地図] 東京: 晴れ、大阪: 雨
[トップ] 東京: 晴れ
サーバーに聞いた回数: 2

画面は同じように3回描き、見える天気も同じです。でも、サーバーには2回しか聞いていません。東京と大阪を1回ずつです。

ステップごとにもう一度

  • ステップ1 — 同じ約束。【天気キャッシュ】も【天気情報】に従います。だから【天気情報】の『天気』を受け取る画面の関数に、そのまま渡せます。
  • ステップ2 — 本物を抱える。 プロキシは本物のサーバーを『サーバー』に抱えています。本物がやる仕事を作り直さず、必要な時に任せます。
  • ステップ3 — 行く前に判断する。〈今日の天気〉は、まず『記憶』という辞書にその都市があるかを〈含むか確認〉で確かめます。あればすぐに返し、ない時だけサーバーに聞いて、その答えを覚えておきます。
  • 変えたのは1行。 画面のコードも、サーバーのコードもそのままです。変わったのは、いちばん下で画面に渡すオブジェクトを『サーバー』から『天気』(プロキシ)にしたことだけです。

プロキシは覚えておくだけ?
いいえ。「本物のオブジェクトへ行く前に何かをする」なら、すべてプロキシです。よく使う種類はこちらです。
覚えておくプロキシ: この例のように、同じ答えを聞き直しません。
守るプロキシ: 権限がなければ、本物のオブジェクトへ行けないように止めます。
後から作るプロキシ: 作るのに時間がかかる本物のオブジェクトを、最初に頼まれた時に初めて作ります。
記録するプロキシ: だれがいつ何を頼んだかを残します。

新しいプロキシを足してみると

「ログインした人だけが天気を見られるように」してほしいと頼まれたとしましょう。

  • 前のやり方: 天気を聞くすべての画面に「ログインしているか?」を入れるか、サーバーを直さなければなりません。
  • プロキシパターン:【天気情報】に従う守るプロキシを1つ作って前に立てれば済みます。覚えておくプロキシの前にさらに立てることもできます。同じ約束に従うので、何重に立ててもかまいません。

やってみよう
上のコードに【ログイン確認】プロキシを足してみてください。【天気情報】に従い、中に別の【天気情報】を1つと『ログイン済み』(真/偽)を抱えます。〈今日の天気〉は、ログインしていなければサーバーに行かずに「ログインが必要です」を返し、ログインしていれば中に抱えたものに任せます。新しい【ログイン確認】(新しい【天気キャッシュ】(『サーバー』), 偽)で作ってトップ画面に渡してみましょう。サーバーに聞いた回数は0になるはずです。

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

  • 本物のオブジェクトが遅かったり高かったりして、同じ頼みをし直したくない時
  • 本物のオブジェクトへ行く前に権限を確かめたり、頼みを記録したりしたい時
  • 本物のオブジェクトを作るのに時間がかかるので、本当に必要になるまで後回しにしたい時
  • 本物のオブジェクトを直せない、またはその仕事と混ぜたくない時

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

いいところ 気になるところ
使う側も本物のオブジェクトも直しません 1回多く経由するので、読んでいく時に1段階増えます
覚える、止める、後から作る、記録するを1か所にまとめられます 覚えておくプロキシは、本物の値が変わっても古い答えを返すことがあります(いつ忘れるかを決める必要があります)
同じ約束なので、何重にも立てられます プロキシのしていることが外から見えないので、なぜ遅いのか・なぜ止められたのかを探しにくいことがあります

早すぎるパターンは毒です。
本物のオブジェクトが速く、止めることも記録することもないなら、そのまま本物を使いましょう。「同じことをまた聞いて遅い」「だれでも呼べてはいけない」のような理由ができてからプロキシを立てても、遅くはありません。

ほかのパターンとの関係

  • デコレーターパターンと形がほとんど同じです。 どちらも同じ約束に従いながら本物のオブジェクトを抱えます。デコレーターは機能を足すために何重にも包み、プロキシは本物へ行く道を調整するために前に立ちます。名前より「なぜ包んだのか」が2つを分けます。
  • アダプターパターンと比べてみましょう。 アダプターは違う形をうちの約束に合わせ、プロキシは同じ形のまま前に立ちます。
  • ファサードパターンとも違います。 ファサードはいくつものオブジェクトをまとめて新しいかんたんな窓口を作り、プロキシはオブジェクト1つの同じ窓口を代わりに守ります。
プレイグラウンド
結果
コードを書いて「実行」ボタンを押してください。