⛳

# デザインパターンを学ぶ #10 プロキシ(Proxy)

に公開

1. はじめに

Proxy(プロキシ)は本体(RealSubject)への代用オブジェクトを用意し、アクセスを間接化するパターン。遅延ロード、アクセス制御、キャッシュ、リトライなどを本体を変更せずに差し込めます。

2. Proxyとは

  • Subject:クライアントが期待する共通インターフェース
  • RealSubject:本体(重い処理・外部IOなど)
  • Proxy:RealSubject と同じインターフェースを持ち、前後処理や条件分岐を肩代わりしてから本体へ委譲

3. 代表的な用途

  • Virtual Proxy:重いオブジェクトを 遅延生成(画像/レポート/接続)
  • Protection Proxy:権限チェックやレート制限
  • Cache/Retry Proxy:外部APIやDBのキャッシュ・リトライ
  • Remote Proxy:遠隔オブジェクトへの窓口(RPC等)

4. 実装例(PHP:外部APIに対するキャッシュ+レート制限付きProxy)

<?php

interface WeatherClient {
    public function get(string $city): array; // 例: ['temp' => 28, 'unit' => 'C']
}

// 本体:実際に外部APIを叩くクライアント(重い/失敗しがち)
final class RealWeatherClient implements WeatherClient {
    public function __construct(private string $endpoint, private string $token) {}

    public function get(string $city): array {
        // 擬似的な外部IO(本来は curl/file_get_contents 等)
        // ここでは例として固定レスポンス
        return ['temp' => 28, 'unit' => 'C', 'city' => $city, 'source' => 'real'];
    }
}

// Proxy:同じインターフェースで、前後に制御を挟む
final class WeatherClientProxy implements WeatherClient {
    private ?WeatherClient $real = null;
    private array $cache = [];             // city => ['data' => ..., 'ts' => time()]
    private int $ttlSec = 60;              // 簡易キャッシュTTL
    private int $lastCallAt = 0;           // レート制限用(最小間隔)
    private int $minIntervalMs = 200;      // 200ms間隔

    public function __construct(
        private string $endpoint,
        private string $token
    ) {}

    public function get(string $city): array {
        // 1) キャッシュ
        if (isset($this->cache[$city])) {
            $hit = $this->cache[$city];
            if (time() - $hit['ts'] < $this->ttlSec) {
                return $hit['data'] + ['source' => 'cache'];
            }
        }

        // 2) レート制限(最低間隔)
        $elapsed = (int) (microtime(true) * 1000) - $this->lastCallAt;
        if ($elapsed < $this->minIntervalMs) {
            usleep(($this->minIntervalMs - $elapsed) * 1000);
        }

        // 3) 遅延生成(Virtual Proxy)
        if ($this->real === null) {
            $this->real = new RealWeatherClient($this->endpoint, $this->token);
        }

        // 4) 本体へ委譲 + 簡易リトライ
        $this->lastCallAt = (int) (microtime(true) * 1000);
        $retries = 2;
        do {
            try {
                $res = $this->real->get($city);
                // 5) キャッシュ保存
                $this->cache[$city] = ['data' => $res, 'ts' => time()];
                return $res;
            } catch (\Throwable $e) {
                if ($retries-- <= 0) throw $e;
                usleep(100 * 1000); // 100ms待って再試行
            }
        } while (true);
    }
}

// 使い方
$client = new WeatherClientProxy('https://api.example.com/weather', 'TOKEN');

// 1回目:本体へ、2回目:キャッシュから
print_r($client->get('Tokyo'));   // → source: real
print_r($client->get('Tokyo'));   // → source: cache

5. メリット

  • 責務分離:本体はコア機能に集中、横断的関心(権限/キャッシュ/レート制限等)はProxyへ
  • 差し替え容易:同じインターフェースなのでDIで本体⇄Proxyを切り替え可能
  • 非侵入的拡張:本体コードを変更せず振る舞い追加(OCP)

6. 注意点

  • 階層が増えるとデバッグが追いにくい(ログ/トレーシング推奨)
  • 乱用するとどのProxyが有効か把握しにくい(命名・構成の一貫性)
  • 共有キャッシュやリトライなど副作用の場所を明確に(スレッド/プロセス/リクエスト境界)

7. 他パターンとの違い

  • Decorator:機能追加が主目的(透過的に積み重ね)。Proxyはアクセスの間接化(権限・遅延・キャッシュ)。
  • Adapter:インターフェース変換。Proxyは同一インターフェース。
  • Facade:サブシステムの簡略窓口。Proxyは同じ窓口の前段で制御。

8. テスト観点

  • 本体が呼ばれる回数(キャッシュヒットで減るか)
  • レート制限や権限チェックが期待通りにブロックするか
  • 例外時のリトライ・フォールバック動作

9. まとめ

Proxyは、本体と同じインターフェースを持つ代用オブジェクトで、アクセスの前段に**制御(キャッシュ/権限/レート制限/遅延生成)**を差し込むパターン。
本体を変えずに運用上の要件を満たせるのが強みです。次は構造パターン残りの Flyweight(フライウェイト) に進むと流れが自然です。

Discussion