春のセール

Proxy

別名:プロキシー

一言でいうと

Proxy プロキシー 代理 構造に関するデザインパターンの一つで 他のオブジェクトの代理 代用を提供します プロキシーは 元のオブジェクトへのアクセスを制御し 元のオブジェクトへリクエストが行く前か後に別の何かを行うようにすることができます

Proxy デザインパターン

問題

いったいなぜオブジェクトへのアクセスを制御したいのかって 一例 システム資源を大量に消費する巨大なオブジェクトがあるとします それは時々必要になりますが 常に必要なわけではありません

Proxy パターンによって解決された問題

データベースのクエリは本当に遅いことがある

遅延初期化 lazy initialization を実装することもできます つまり オブジェクトを実際に必要な時まで待って作成します すべてのオブジェクトのクライアントが 何らかの遅延初期化コードを実行する必要があります 残念ながら これにより多くのコードが重複する可能性があります

理想的な世界では このコードをオブジェクトのクラスに直接置きたいところですが それがいつも可能とは限りません たとえば そのクラスはソースコード非公開の外部開発ライブラリーの一部なのかもしれません

解決策

Proxy パターンでは 元のサービス・オブジェクトと同じインターフェースで新しいプロキシー・クラスを作成します 次に アプリを変更して 元のオブジェクトのすべてのクライアントに プロキシーのオブジェクトを渡すようにします クライアントからのリクエストを受け取ると プロキシーは実際のサービス・オブジェクトを作成し すべての作業を委任します

Proxy パターンによる解決策

プロキシーはデータベース・オブジェクトのふりをする 遅延初期化やキャッシュ処理を クライアントや実際のデータベース・オブジェクトに知られることなく行える

しかし そんなことをする利点は何でしょう クラスの一次的な仕事の前後に何かを実行する必要がある場合 プロキシーは元クラスを変更せずにこれを行うことができます プロキシーは元のクラスと同じインターフェースを実装しているため 実際のサービス・オブジェクトを期待しているクライアントに渡すことができます

現実世界でのたとえ

クレジットカードは、現金の束のプロキシー

クレジットカードは まるで現金と同じように 支払いに使用可能

クレジットカードは 銀行口座のプロキシーであり 銀行口座は 現金の束のプロキシーです どちらも同じインターフェース つまり支払い方法を実装しています 消費者は 大量の現金を持ち歩く必要がないので 気分が楽です 取引先からの入金が店の銀行口座に電子的に追加されるので 現金を失ったり 銀行に行く途中で強奪されるリスクもなくなり 店主も幸せです

構造

Proxy デザインパターンの構造Proxy デザインパターンの構造
  1. サービス・インターフェース Service Interface サービスのインターフェースを宣言します プロキシーは サービス・オブジェクトのふりをするために このインターフェースに沿わなければなりません

  2. サービス Service 何らかの役に立つビジネス・ロジックを提供するクラスです

  3. プロキシー Proxy クラスには サービス・オブジェクトを指す参照フィールドがあります プロキシーの行うべき処理 たとえば 遅延初期化 ロギング アクセス制御 キャッシュ処理など が完了すると プロキシーは サービス・オブジェクトにリクエストを渡します

    通常 プロキシーはサービス・オブジェクトのライフサイクルを完全に管理します

  4. クライアント Client 同じインターフェースを介してサービスとプロキシーの両方で動作します こうすることで サービス・オブジェクトを期待するどんなコードにでもプロキシーを渡すことができます

擬似コード

この例では どのように Proxy パターンが 外部作成の YouTube 統合ライブラリーに遅延初期化とキャッシグを導入するのに役立つかを説明します

Proxy パターンの例の構造

プロキシーを使用してサービスの結果をキャッシュ

ライブラリーは ビデオのダウンロードをするクラスを提供します しかしながらそれは かなり非効率です もしクライアント・アプリケーションが同じビデオを複数回要求すると ライブラリーは それを何回もダウンロードします ファイルをキャッシングして再利用をすることはしません

プロキシー・クラスは元のダウンロード・クラスと同じインターフェースを実装し すべての作業を委任します ただし ダウンロードしたファイルを記録し アプリが同じ動画を複数回リクエストした時は キャッシュされた結果を返します

// リモート・サービスのインターフェース。
interface ThirdPartyYouTubeLib is
    method listVideos()
    method getVideoInfo(id)
    method downloadVideo(id)

// サービス・コネクターの具体的な実装。このクラスのメソッドは、YouTube か
// ら情報を要求できる。リクエストの処理速度は、ユーザーのインターネット接
// 続状況と YoutTube 側の状況に依存。同時にいくつものリクエストがあると、
// 仮に全部が同じ情報のリクエストだったとしても、アプリケーションは減速す
// る。
class ThirdPartyYouTubeClass implements ThirdPartyYouTubeLib is
    method listVideos() is
        // YouTube に API リクエストを送信。

    method getVideoInfo(id) is
        // ビデオに関するメタデータの取得。

    method downloadVideo(id) is
        // YouTube からビデオ・ファイルをダウンロード。

// 帯域幅を節約するために、リクエストの結果をキャッシュし、しばらくの間保
// 存することが可能。しかし、そのようなコードを直接サービス・クラスに入れ
// ることは不可能な場合あり。たとえば、外部作成のライブラリーの一部として
// 提供されていたり、クラスが final と定義されていたりする場合。サービス・
// クラスと同じインターフェースを実装する新しいプロキシー・クラスを作り、
// キャッシュのためのコードを追加するのは、このため。本当のリクエストを
// 送信する時にだけ、サービス・オブジェクトにリクエストを委任する。
class CachedYouTubeClass implements ThirdPartyYouTubeLib is
    private field service: ThirdPartyYouTubeLib
    private field listCache, videoCache
    field needReset

    constructor CachedYouTubeClass(service: ThirdPartyYouTubeLib) is
        this.service = service

    method listVideos() is
        if (listCache == null || needReset)
            listCache = service.listVideos()
        return listCache

    method getVideoInfo(id) is
        if (videoCache == null || needReset)
            videoCache = service.getVideoInfo(id)
        return videoCache

    method downloadVideo(id) is
        if (!downloadExists(id) || needReset)
            service.downloadVideo(id)

// 以前はサービス・オブジェクトと直接機能していた GUI のクラスは、あるイ
// ンターフェースを通してサービス・オブジェクトと機能する限り変更不要。本
// 物のサービス・オブジェクトに代わってプロキシー・オブジェクトを安全に渡
// すことが可能。これは、両方のクラスが同じインターフェースを実装するため。
class YouTubeManager is
    protected field service: ThirdPartyYouTubeLib

    constructor YouTubeManager(service: ThirdPartyYouTubeLib) is
        this.service = service

    method renderVideoPage(id) is
        info = service.getVideoInfo(id)
        // ビデオ・ページを描画。

    method renderListPanel() is
        list = service.listVideos()
        // ビデオのサムネールをリスト表示。

    method reactOnUserInput() is
        renderVideoPage()
        renderListPanel()

// アプリケーションは、プロキシーをその場で構成可能。
class Application is
    method init() is
        aYouTubeService = new ThirdPartyYouTubeClass()
        aYouTubeProxy = new CachedYouTubeClass(aYouTubeService)
        manager = new YouTubeManager(aYouTubeProxy)
        manager.reactOnUserInput()

適応性

Proxy パターンの用途はたくさんあります 最もよく使われる用途を見てみましょう

遅延初期化 仮想プロキシー たまにしか必要のないヘビー級のサービス・オブジェクトが常に使用可能となっている場合です

アプリが起動した時にオブジェクトを作成する代わりに オブジェクトの初期化を実際に必要な時まで遅らせることができます

アクセス・コントロール 保護プロキシー これは 特定のクライアントだけがサービス・オブジェクトを使用できるようにしたい場合です たとえば 対象オブジェクトがオペレーティングシステムの基幹部分で クライアントが様々な起動アプリケーション 悪意のあるものを含む である場合

プロキシーは クライアントの資格情報が何らかの条件に一致する場合にのみ リクエストをサービス・オブジェクトに渡すことができます

リモート・サービスのローカル実行 リモート・プロキシー これは サービス・オブジェクトがリモート・サーバー上にある場合です

この場合 プロキシーは ネットワークを介してクライアントのリクエストを渡します プロキシーがネットワークに関わる面倒で厄介な処理を実行します

ロギング・リクエスト ロギング・プロキシー これは サービス・オブジェクトへのリクエストの履歴を残したい場合です

プロキシーは 各リクエストをサービスに渡す前にログに記録できます

リクエスト結果のキャッシュ キャッシング・プロキシー これは クライアントのリクエストの結果をキャッシュし このキャシュのライフサイクルを管理する必要がある場合です これは特に リクエストの結果が非常に大きい場合に役立ちます

プロキシーは 定期的になされ 同じ結果に至るリクエストのキャッシュを実装できます プロキシーはリクエストのパラメーターをキャッシュのキーとして使用するかもしれません

スマート参照 これは 重量級オブジェクトを それを使うクライアントがなくなったら破棄できるようにしたい場合に重宝します

プロキシーは サービス・オブジェクトの参照をしたクライアントとその結果を記録することができます 時々プロキシーは クライアント一つずつに対して まだ活動中かどうかを調べます クライアントのリストが空になったら プロキシーはサービス・オブジェクトを破棄し そのシステム資源を解放するかもしれません

プロキシーは クライアントがサービス・オブジェクトを変更したかどうかを追跡することもできます その後 変更されていないオブジェクトは他のクライアントによって再利用されるかもしれません

実装方法

  1. 既存のサービス・インターフェースがない場合は プロキシーとサービス・オブジェクトを交換可能にするために インターフェースを作成します サービス・クラスからインターフェースを抽出することは 必ずしも可能ではありません というのは そのサービスを使用しているすべてのクライアントを変更する必要が出てくるからです 次善の策としては プロキシーをサービス・クラスのサブクラスにすることです これによりサービスのインターフェースを継承します

  2. プロキシー・クラスを作成します サービスへの参照を保存するためのフィールドを持っている必要があります 通常 プロキシーは サービスを作成し その後のライフサイクルを生涯に渡り管理します 稀にですが クライアントはコンストラクターを介してサービスをプロキシーに渡すことがあります

  3. 目的に応じて プロキシーにいくつかのメソッドを実装します ほとんどの場合 何らかの作業を行った後 プロキシーは仕事をサービス・オブジェクトに委任します

  4. 生成メソッドを作成することを検討してください それは クライアントにプロキシーを渡すべきか 本物のサービスを渡すべきかを決めます プロキシー・クラス中の静的メソッドとして実装することも 一人前のファクトリー・メソッドとして実装することもできます

  5. サービス・オブジェクトの遅延初期化の実装を検討してください

長所と短所

  • クライアントに知られずにサービス・オブジェクトを制御可能
  • クライアントが関知しないのであれば サービス・オブジェクトのライフサイクルの管理が可能
  • サービス・オブジェクトの準備中 不足中でも プロキシーは機能
  • サービスやクライアントを変更せずとも新規プロキシーを導入可能
  • 多くの新しいクラスを導入する必要のため コードがより複雑になる可能性あり
  • サービスからの応答遅延の可能性

他のパターンとの関係

  • Adapter はラップされたオブジェクトに対しては異なるインターフェースを提供し Proxy は同じインターフェースを提供し Decorator は強化したインターフェースを提供します

  • Facade どちらも複雑な実体の緩衝材として機能し 初期化も行うという意味で Proxy と似てます Facade と異なり Proxy はそのサービス・オブジェクトと同じインターフェースを持ち 両者は交換可能です

  • DecoratorProxy とは似たような構造をしていますが その意図は非常に異なります どちらのパターンも 合成 コンポジション の原則に基づいており あるオブジェクトが仕事の一部を別のオブジェクトに委任します 違いは Proxy は通常そのサービス・オブジェクトのライフサイクルの管理を行うのに対し Decorators では 常にクライアントが管理するという点です

コード例

Proxy を C# で Proxy を C++ で Proxy を Go で Proxy を Java で Proxy を PHP で Proxy を Python で Proxy を Ruby で Proxy を Rust で Proxy を Swift で Proxy を TypeScript で