2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

業務レベルの実例で理解するProxyパターン

Last updated at Posted at 2024-11-04

はじめに

この記事のゴールは、適切な場面でProxyパターンを使えるようになることです。
不適切な場面におけるデザインパターンの適用は大きな技術的負債をもたらしますので気をつけましょう。

対象読者は、Proxyパターンのことをなんとなく知っているものの仕事では自信を持って使えない人です。

筆者はPHPerなのでサンプルコードはPHPです。

完成版はこちらです。

Proxyパターンとは

以下がわかりやすいです。

Proxyパターンは、すでにあるオブジェクト関係の間に、透過的に別のオブジェクトを割り込ませて、元のコードを変更せずに、同じ機能呼び出しの振る舞いを拡張するためのパターンです。

田中ひさてる (2022). ちょうぜつソフトウェア設計入門――PHPで理解するオブジェクト指向の活用 技術評論社

クラス図は以下のようになります。

既存メソッドの振る舞いはそのままにし、新たなメソッドを追加するのはDecoratorパターンです。
混合しやすいので注意しましょう。

実例

よくあるケース: APIクライアントを実装

Guzzleを使ってAPIをコールするケースはよくあると思います。

典型的な実装は以下のようになります。

src/Feature/DesignPattern/Proxy/ApiClient.php
<?php

declare(strict_types=1);

namespace Qps\Feature\DesignPattern\Proxy;

use GuzzleHttp\ClientInterface;

class ApiClient
{
    private ClientInterface $client;

    public function __construct(ClientInterface $client)
    {
        $this->client = $client;
    }

    public function request(): string
    {
        $response = $this->client->request('get', 'https://httpbin.org/get');

        return $response->getBody()->getContents();
    }
}

ClientInterfaceの具象は以下のようにGuzzleのClientを注入しています。

bootstrap/container.php
<?php

declare(strict_types=1);

$c = new Illuminate\Container\Container();

$c->bind(
    \Qps\Feature\DesignPattern\Proxy\ApiClient::class,
    function () {
        $client = new GuzzleHttp\Client();
        return new \Qps\Feature\DesignPattern\Proxy\ApiClient($client);
    }
);

return $c;

利用側のコードはこちらです。

bin/app.php
<?php

declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';
$c = require __DIR__ . '/../bootstrap/container.php';

$apiClient = $c->make(\Qps\Feature\DesignPattern\Proxy\ApiClient::class);
var_dump($apiClient->request());

クラス図はこちらです。

ここまでのコードはこちらです。

仕様追加が発生: HTTPステータスコードが429のときリトライしたい

たとえば、SlackのAPIはけっこう簡単に429が発生します。
このとき、レスポンスヘッダーを見れば必要な待ち時間がわかるので、その時間待ったうえでリトライする必要があります。
こういったことの実現を考えます。

ApiClientを書き換えてもいいならいくらでもやりようはあります。
しかし、Proxyパターンを使うと書き換えはDIコンテナのバインド処理だけで済みます。

それでは見ていきましょう。

OCPに準じつつ拡張: Proxyパターンを利用

まずはクラス図を示します。

先ほどの実装から追加になったのはRetryableClientです。

src/Feature/DesignPattern/Proxy/RetryableClient.php
<?php

declare(strict_types=1);

namespace Qps\Feature\DesignPattern\Proxy;

use GuzzleHttp\ClientInterface;
use GuzzleHttp\Middleware;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class RetryableClient implements ClientInterface
{
    private ClientInterface $client;
    private const int MAX_RETRY_COUNT = 3;

    public function __construct(ClientInterface $client)
    {
        $client->getConfig('handler')->push(Middleware::retry($this->decider(), $this->delay()));
        $this->client = $client;
    }

    private function decider(): \Closure
    {
        return function (int $retries, Request $request, Response $response) {
            if ($retries >= self::MAX_RETRY_COUNT) {
                return false;
            }
            $statusCode = $response->getStatusCode();
            if ($statusCode === 429) {
                return true;
            }
            return false;
        };
    }

    private function delay(): \Closure
    {
        return function (int $retries, Response $response): float {
            return 0;
        };
    }

    /**
     * @inheritDoc
     */
    public function send(RequestInterface $request, array $options = []): ResponseInterface
    {
        return $this->client->send($request, $options);
    }

    /**
     * @inheritDoc
     */
    public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface
    {
        return $this->client->sendAsync($request, $options);
    }

    /**
     * @inheritDoc
     */
    public function request(string $method, $uri, array $options = []): ResponseInterface
    {
        return $this->client->request($method, $uri, $options);
    }

    /**
     * @inheritDoc
     */
    public function requestAsync(string $method, $uri, array $options = []): PromiseInterface
    {
        return $this->client->requestAsync($method, $uri, $options);
    }

    /**
     * @inheritDoc
     */
    public function getConfig(?string $option = null)
    {
        return $this->getConfig($option);
    }
}

Guzzleのミドルウェアという機能を使って、指定した回数リトライできるようにします。

そして、DIコンテナのバインド処理を修正します。

bootstrap/container.php
<?php

declare(strict_types=1);

$c = new Illuminate\Container\Container();

$c->bind(
    \Qps\Feature\DesignPattern\Proxy\ApiClient::class,
    function () {
-        $client = new GuzzleHttp\Client();
+        $client = new \Qps\Feature\DesignPattern\Proxy\RetryableClient(new GuzzleHttp\Client());
        return new \Qps\Feature\DesignPattern\Proxy\ApiClient($client);
    }
);

return $c;

冒頭で掲載したリンクと同じですが、完成版のコードはこちらです。

さいごに

今回の事例のように、同じ機能呼び出しの振る舞いを拡張したいときは、Proxyパターンを使うといい感じに拡張できてよきです。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?