はじめに
この記事のゴールは、適切な場面でProxyパターンを使えるようになることです。
不適切な場面におけるデザインパターンの適用は大きな技術的負債をもたらしますので気をつけましょう。
対象読者は、Proxyパターンのことをなんとなく知っているものの仕事では自信を持って使えない人です。
筆者はPHPerなのでサンプルコードはPHPです。
完成版はこちらです。
Proxyパターンとは
以下がわかりやすいです。
Proxyパターンは、すでにあるオブジェクト関係の間に、透過的に別のオブジェクトを割り込ませて、元のコードを変更せずに、同じ機能呼び出しの振る舞いを拡張するためのパターンです。
田中ひさてる (2022). ちょうぜつソフトウェア設計入門――PHPで理解するオブジェクト指向の活用 技術評論社
クラス図は以下のようになります。
既存メソッドの振る舞いはそのままにし、新たなメソッドを追加するのはDecoratorパターンです。
混合しやすいので注意しましょう。
実例
よくあるケース: APIクライアントを実装
Guzzleを使ってAPIをコールするケースはよくあると思います。
典型的な実装は以下のようになります。
<?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
を注入しています。
<?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;
利用側のコードはこちらです。
<?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
です。
<?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コンテナのバインド処理を修正します。
<?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パターンを使うといい感じに拡張できてよきです。