#10 デザインパターン入門
Adapter——互換性のないクラスをつなぐ
問題: 外部ライブラリのインターフェースが合わない
決済機能を実装するとき、最初はStripeだけだったのに、あとから「PayPalも使いたい」「GMO後払いも追加して」という要件が来ることがあります。それぞれのSDKはメソッド名も引数も異なります。
// Stripeの使い方
$stripe = new \Stripe\PaymentIntent();
$stripe->create(['amount' => 5800, 'currency' => 'jpy']);
// PayPalの使い方(まったく異なるAPI)
$paypal = new \PayPal\Api\Payment();
$paypal->setAmount(new \PayPal\Api\Amount(['total' => '58.00', 'currency' => 'JPY']));
$paypal->create($apiContext);
// GMO後払いの使い方(さらに異なる)
$gmo = new \GmoPg\PostPayClient();
$gmo->execTran(['jobCd' => 'AUTH', 'amount' => 5800]);
それぞれを直接呼び出すと、アプリケーションのコードが外部ライブラリに強く依存します。ライブラリを変更したとき、呼び出し箇所を全部修正する羽目になります。
パターン: Adapter
Adapterパターンは、互換性のないインターフェースを持つクラスを、既存のインターフェースに合わせてラップするパターンです。コンセントの変換アダプターと同じ発想です。
+-----------------------------+
| PaymentGateway |
| <<interface>> |
+-----------------------------+
| + charge(amount: int): bool |
| + refund(txId: string): bool|
+-----------------------------+
△
_________|___________
| | |
StripeAdapter PayPalAdapter GmoAdapter
(Stripeをラップ)(PayPalをラップ)(GMOをラップ)
PHP 8.x での実装(オブジェクトアダプター)
まず統一インターフェースを定義します。
<?php
// アプリケーションが依存するインターフェース
interface PaymentGateway
{
/**
* 決済を実行する
* @throws \RuntimeException 決済失敗時
*/
public function charge(int $amountJpy, string $orderId): string;
/**
* 返金する
*/
public function refund(string $transactionId, int $amountJpy): bool;
public function getName(): string;
}
各決済サービスをラップするアダプターを実装します。
<?php
// Stripeアダプター(オブジェクトアダプター)
class StripeAdapter implements PaymentGateway
{
private \Stripe\StripeClient $stripe;
public function __construct(string $apiKey)
{
$this->stripe = new \Stripe\StripeClient($apiKey);
}
public function charge(int $amountJpy, string $orderId): string
{
// Stripe固有のAPIを統一インターフェースに合わせる
$intent = $this->stripe->paymentIntents->create([
'amount' => $amountJpy,
'currency' => 'jpy',
'metadata' => ['order_id' => $orderId],
]);
if ($intent->status !== 'succeeded') {
throw new \RuntimeException("Stripe決済失敗: {$intent->status}");
}
return $intent->id; // トランザクションID
}
public function refund(string $transactionId, int $amountJpy): bool
{
$refund = $this->stripe->refunds->create([
'payment_intent' => $transactionId,
'amount' => $amountJpy,
]);
return $refund->status === 'succeeded';
}
public function getName(): string { return 'Stripe'; }
}
// PayPalアダプター
class PayPalAdapter implements PaymentGateway
{
private \PayPalCheckoutSdk\Core\PayPalHttpClient $client;
public function __construct(string $clientId, string $clientSecret)
{
$environment = new \PayPalCheckoutSdk\Core\SandboxEnvironment($clientId, $clientSecret);
$this->client = new \PayPalCheckoutSdk\Core\PayPalHttpClient($environment);
}
public function charge(int $amountJpy, string $orderId): string
{
// PayPal固有のAPIを統一インターフェースに変換
$request = new \PayPalCheckoutSdk\Orders\OrdersCreateRequest();
$request->body = [
'intent' => 'CAPTURE',
'purchase_units' => [[
'amount' => ['currency_code' => 'JPY', 'value' => (string) $amountJpy],
'reference_id' => $orderId,
]],
];
$response = $this->client->execute($request);
return $response->result->id;
}
public function refund(string $transactionId, int $amountJpy): bool
{
// PayPalの返金処理
return true; // 簡略化
}
public function getName(): string { return 'PayPal'; }
}
使う側のコードはどの決済サービスを使っても同じです。
class CheckoutService
{
public function __construct(
private readonly PaymentGateway $gateway,
) {}
public function checkout(int $orderId, int $amount): string
{
$transactionId = $this->gateway->charge($amount, (string) $orderId);
echo "[{$this->gateway->getName()}] 決済完了: {$transactionId}\n";
return $transactionId;
}
}
// 使用例: ゲートウェイを切り替えるだけ
$service = new CheckoutService(new StripeAdapter(env('STRIPE_KEY')));
$service->checkout(101, 5800);
$service = new CheckoutService(new PayPalAdapter(env('PAYPAL_ID'), env('PAYPAL_SECRET')));
$service->checkout(102, 3200);
クラスアダプターとオブジェクトアダプター
Adapterには2つの実装パターンがあります。
オブジェクトアダプター(推奨)
アダプターがラップ対象のオブジェクトを内部に持つパターン。コンストラクターで受け取り、委譲します。
// オブジェクトアダプター(委譲)
class StripeAdapter implements PaymentGateway
{
public function __construct(
private readonly \Stripe\StripeClient $stripe, // 外部クラスを保持
) {}
public function charge(int $amount, string $orderId): string
{
return $this->stripe->paymentIntents->create([...])->id; // 委譲
}
}
クラスアダプター(継承)
アダプターがラップ対象を継承するパターン。PHPでは多重継承がないため使いにくいことが多いです。
// クラスアダプター(継承)
// PHPは単一継承なので、interfaceとの組み合わせが必要
class StripeAdapter extends \Stripe\StripeClient implements PaymentGateway
{
public function charge(int $amount, string $orderId): string
{
// 親クラスのメソッドを直接呼べる
return $this->paymentIntents->create([...])->id;
}
}
PHPではオブジェクトアダプターの方が柔軟性が高く、一般的に推奨されます。
Laravelでの応用(ファイルストレージ)
LaravelのStorageファサードはAdapterパターンの典型例です。ローカル・S3・FTPなど異なるストレージを同一インターフェースで扱います。
// インターフェースは同じ、裏側のストレージが切り替わる
Storage::disk('local')->put('file.txt', '内容');
Storage::disk('s3')->put('file.txt', '内容');
Storage::disk('ftp')->put('file.txt', '内容');
// 環境変数でデフォルトのディスクを切り替え
// FILESYSTEM_DISK=s3 にするだけで全Storage呼び出しがS3に向く
Storage::put('file.txt', '内容');
独自のストレージアダプターを実装することもできます。
// config/filesystems.php
'disks' => [
'custom' => [
'driver' => 'custom_storage', // カスタムドライバー
'root' => storage_path('custom'),
],
],
// カスタムドライバーの登録
Storage::extend('custom_storage', function ($app, $config) {
return new \League\Flysystem\Filesystem(new MyCustomAdapter($config));
});
サードパーティSDKのラッピング
外部SDKをアダプターで包む際の基本的なパターンです。
// メール配信サービスのアダプター
interface MailerInterface
{
public function send(string $to, string $subject, string $body): void;
}
class MailgunAdapter implements MailerInterface
{
public function __construct(
private readonly \Mailgun\Mailgun $mailgun,
private readonly string $domain,
) {}
public function send(string $to, string $subject, string $body): void
{
$this->mailgun->messages()->send($this->domain, [
'from' => 'noreply@example.com',
'to' => [$to],
'subject' => $subject,
'html' => $body,
]);
}
}
class SesAdapter implements MailerInterface
{
public function __construct(private readonly \Aws\Ses\SesClient $ses) {}
public function send(string $to, string $subject, string $body): void
{
$this->ses->sendEmail([
'Destination' => ['ToAddresses' => [$to]],
'Message' => [
'Body' => ['Html' => ['Data' => $body]],
'Subject' => ['Data' => $subject],
],
'Source' => 'noreply@example.com',
]);
}
}
まとめ
- Adapterパターンは互換性のないインターフェースを統一するラッパーを作る
- 外部ライブラリの変更が内部コードに影響しなくなる(依存の逆転)
- オブジェクトアダプター(委譲)がPHPでは主流
- Laravelの
StorageファサードはAdapterパターンの代表例
次回はTemplate Methodパターンで、アルゴリズムの骨格を抽象クラスで定義する方法を解説します。