Proxy
一言でいうと
Proxy (プロキシー、 代理) は、 構造に関するデザインパターンの一つで、 他のオブジェクトの代理、 代用を提供します。 プロキシーは、 元のオブジェクトへのアクセスを制御し、 元のオブジェクトへリクエストが行く前か後に別の何かを行うようにすることができます。
問題
いったいなぜオブジェクトへのアクセスを制御したいのかって? 一例: システム資源を大量に消費する巨大なオブジェクトがあるとします。 それは時々必要になりますが、 常に必要なわけではありません。
遅延初期化 (lazy initialization) を実装することもできます。 つまり、 オブジェクトを実際に必要な時まで待って作成します。 すべてのオブジェクトのクライアントが、 何らかの遅延初期化コードを実行する必要があります。 残念ながら、 これにより多くのコードが重複する可能性があります。
理想的な世界では、 このコードをオブジェクトのクラスに直接置きたいところですが、 それがいつも可能とは限りません。 たとえば、 そのクラスはソースコード非公開の外部開発ライブラリーの一部なのかもしれません。
解決策
Proxy パターンでは、 元のサービス・オブジェクトと同じインターフェースで新しいプロキシー・クラスを作成します。 次に、 アプリを変更して、 元のオブジェクトのすべてのクライアントに、 プロキシーのオブジェクトを渡すようにします。 クライアントからのリクエストを受け取ると、 プロキシーは実際のサービス・オブジェクトを作成し、 すべての作業を委任します。
しかし、 そんなことをする利点は何でしょう? クラスの一次的な仕事の前後に何かを実行する必要がある場合、 プロキシーは元クラスを変更せずにこれを行うことができます。 プロキシーは元のクラスと同じインターフェースを実装しているため、 実際のサービス・オブジェクトを期待しているクライアントに渡すことができます。
現実世界でのたとえ
クレジットカードは、 銀行口座のプロキシーであり、 銀行口座は、 現金の束のプロキシーです。 どちらも同じインターフェース、 つまり支払い方法を実装しています。 消費者は、 大量の現金を持ち歩く必要がないので、 気分が楽です。 取引先からの入金が店の銀行口座に電子的に追加されるので、 現金を失ったり、 銀行に行く途中で強奪されるリスクもなくなり、 店主も幸せです。
構造
-
サービス・インターフェース (Service Interface) は、 サービスのインターフェースを宣言します。 プロキシーは、 サービス・オブジェクトのふりをするために、 このインターフェースに沿わなければなりません。
-
サービス (Service) は、 何らかの役に立つビジネス・ロジックを提供するクラスです。
-
プロキシー (Proxy) クラスには、 サービス・オブジェクトを指す参照フィールドがあります。 プロキシーの行うべき処理 (たとえば、 遅延初期化、 ロギング、 アクセス制御、 キャッシュ処理など) が完了すると、 プロキシーは、 サービス・オブジェクトにリクエストを渡します。
通常、 プロキシーはサービス・オブジェクトのライフサイクルを完全に管理します。
-
クライアント (Client) は、 同じインターフェースを介してサービスとプロキシーの両方で動作します。 こうすることで、 サービス・オブジェクトを期待するどんなコードにでもプロキシーを渡すことができます。
擬似コード
この例では、 どのように Proxy パターンが、 外部作成の YouTube 統合ライブラリーに遅延初期化とキャッシグを導入するのに役立つかを説明します。
ライブラリーは、 ビデオのダウンロードをするクラスを提供します。 しかしながらそれは、 かなり非効率です。 もしクライアント・アプリケーションが同じビデオを複数回要求すると、 ライブラリーは、 それを何回もダウンロードします。 ファイルをキャッシングして再利用をすることはしません。
プロキシー・クラスは元のダウンロード・クラスと同じインターフェースを実装し、 すべての作業を委任します。 ただし、 ダウンロードしたファイルを記録し、 アプリが同じ動画を複数回リクエストした時は、 キャッシュされた結果を返します。
適応性
Proxy パターンの用途はたくさんあります。 最もよく使われる用途を見てみましょう。
遅延初期化 (仮想プロキシー)。 たまにしか必要のないヘビー級のサービス・オブジェクトが常に使用可能となっている場合です。
アプリが起動した時にオブジェクトを作成する代わりに、 オブジェクトの初期化を実際に必要な時まで遅らせることができます。
アクセス・コントロール (保護プロキシー)。 これは、 特定のクライアントだけがサービス・オブジェクトを使用できるようにしたい場合です。 たとえば、 対象オブジェクトがオペレーティングシステムの基幹部分で、 クライアントが様々な起動アプリケーション (悪意のあるものを含む) である場合。
プロキシーは、 クライアントの資格情報が何らかの条件に一致する場合にのみ、 リクエストをサービス・オブジェクトに渡すことができます。
リモート・サービスのローカル実行 (リモート・プロキシー)。 これは、 サービス・オブジェクトがリモート・サーバー上にある場合です。
この場合、 プロキシーは、 ネットワークを介してクライアントのリクエストを渡します。 プロキシーがネットワークに関わる面倒で厄介な処理を実行します。
ロギング・リクエスト (ロギング・プロキシー)。 これは、 サービス・オブジェクトへのリクエストの履歴を残したい場合です。
プロキシーは、 各リクエストをサービスに渡す前にログに記録できます。
リクエスト結果のキャッシュ (キャッシング・プロキシー)。 これは、 クライアントのリクエストの結果をキャッシュし、 このキャシュのライフサイクルを管理する必要がある場合です。 これは特に、 リクエストの結果が非常に大きい場合に役立ちます。
プロキシーは、 定期的になされ、 同じ結果に至るリクエストのキャッシュを実装できます。 プロキシーはリクエストのパラメーターをキャッシュのキーとして使用するかもしれません。
スマート参照。 これは、 重量級オブジェクトを、 それを使うクライアントがなくなったら破棄できるようにしたい場合に重宝します。
プロキシーは、 サービス・オブジェクトの参照をしたクライアントとその結果を記録することができます。 時々プロキシーは、 クライアント一つずつに対して、 まだ活動中かどうかを調べます。 クライアントのリストが空になったら、 プロキシーはサービス・オブジェクトを破棄し、 そのシステム資源を解放するかもしれません。
プロキシーは、 クライアントがサービス・オブジェクトを変更したかどうかを追跡することもできます。 その後、 変更されていないオブジェクトは他のクライアントによって再利用されるかもしれません。
実装方法
-
既存のサービス・インターフェースがない場合は、 プロキシーとサービス・オブジェクトを交換可能にするために、 インターフェースを作成します。 サービス・クラスからインターフェースを抽出することは、 必ずしも可能ではありません。 というのは、 そのサービスを使用しているすべてのクライアントを変更する必要が出てくるからです。 次善の策としては、 プロキシーをサービス・クラスのサブクラスにすることです。 これによりサービスのインターフェースを継承します。
-
プロキシー・クラスを作成します。 サービスへの参照を保存するためのフィールドを持っている必要があります。 通常、 プロキシーは、 サービスを作成し、 その後のライフサイクルを生涯に渡り管理します。 稀にですが、 クライアントはコンストラクターを介してサービスをプロキシーに渡すことがあります。
-
目的に応じて、 プロキシーにいくつかのメソッドを実装します。 ほとんどの場合、 何らかの作業を行った後、 プロキシーは仕事をサービス・オブジェクトに委任します。
-
生成メソッドを作成することを検討してください。 それは、 クライアントにプロキシーを渡すべきか、 本物のサービスを渡すべきかを決めます。 プロキシー・クラス中の静的メソッドとして実装することも、 一人前のファクトリー・メソッドとして実装することもできます。
-
サービス・オブジェクトの遅延初期化の実装を検討してください。
長所と短所
- クライアントに知られずにサービス・オブジェクトを制御可能 。
- クライアントが関知しないのであれば、 サービス・オブジェクトのライフサイクルの管理が可能。
- サービス・オブジェクトの準備中、 不足中でも、 プロキシーは機能。
- 開放閉鎖の原則。 サービスやクライアントを変更せずとも新規プロキシーを導入可能。
- 多くの新しいクラスを導入する必要のため、 コードがより複雑になる可能性あり。
- サービスからの応答遅延の可能性。
他のパターンとの関係
-
Adapter はラップされたオブジェクトに対しては異なるインターフェースを提供し、 Proxy は同じインターフェースを提供し、 Decorator は強化したインターフェースを提供します。
-
Facade は、 どちらも複雑な実体の緩衝材として機能し、 初期化も行うという意味で、 Proxy と似てます。 Facade と異なり、 Proxy はそのサービス・オブジェクトと同じインターフェースを持ち、 両者は交換可能です。
-
Decorator と Proxy とは似たような構造をしていますが、 その意図は非常に異なります。 どちらのパターンも、 合成 (コンポジション) の原則に基づいており、 あるオブジェクトが仕事の一部を別のオブジェクトに委任します。 違いは、 Proxy は通常そのサービス・オブジェクトのライフサイクルの管理を行うのに対し、 Decorators では、 常にクライアントが管理するという点です。