#07 デザインパターン入門

Decorator——機能を動的に追加する

問題: 継承では組み合わせが爆発する

サービスクラスに「ログ出力」「キャッシュ」「認証チェック」を追加したいとします。継承で解決しようとすると…

// 継承による解決 → クラスが爆発する
class UserService { ... }
class LoggingUserService extends UserService { ... }
class CachingUserService extends UserService { ... }
class AuthUserService extends UserService { ... }
class LoggingCachingUserService extends LoggingUserService { ... }  // ← 組み合わせが無限に
class LoggingCachingAuthUserService extends LoggingCachingUserService { ... }

「ログ + キャッシュ」「ログ + 認証」「ログ + キャッシュ + 認証」と組み合わせが増えるにつれ、クラス数が爆発します。


パターン: Decorator

Decoratorパターンは、オブジェクトをラップすることで、クラスを変更せずに機能を追加するパターンです。ラッパー(デコレーター)はラップ対象と同じインターフェースを実装するため、元のオブジェクトの代わりに使えます。

+-------------------------------+
|   UserServiceInterface       |
+-------------------------------+
| + findById(id: int): User     |
| + create(data: array): User   |
+-------------------------------+

     _________|__________
    |                    |
UserService         Decorator (abstract)
(具体実装)          | - inner: UserServiceInterface |
                    +----------------------------+

                 ________|________
                |                 |
          LoggingDecorator   CachingDecorator

PHP 8.x での実装

まずインターフェースと具体的な実装を定義します。

<?php

interface UserServiceInterface
{
    public function findById(int $id): ?array;
    public function create(array $data): array;
}

// 本体のサービス実装
class UserService implements UserServiceInterface
{
    public function findById(int $id): ?array
    {
        // DBからユーザーを取得する処理
        echo "DB: ユーザー {$id} を取得\n";
        return ['id' => $id, 'name' => 'Alice'];
    }

    public function create(array $data): array
    {
        echo "DB: ユーザーを作成\n";
        return array_merge(['id' => random_int(1, 1000)], $data);
    }
}

次にデコレーターを実装します。デコレーターは同じインターフェースを実装し、内部に「ラップしたいオブジェクト」を持ちます。

<?php

// ログ出力デコレーター
class LoggingUserService implements UserServiceInterface
{
    public function __construct(
        private readonly UserServiceInterface $inner,
    ) {}

    public function findById(int $id): ?array
    {
        echo "[LOG] findById({$id}) を呼び出しました\n";
        $result = $this->inner->findById($id);
        echo "[LOG] findById({$id}) が完了しました\n";
        return $result;
    }

    public function create(array $data): array
    {
        echo "[LOG] create() を呼び出しました\n";
        $result = $this->inner->create($data);
        echo "[LOG] create() が完了: ID={$result['id']}\n";
        return $result;
    }
}

// キャッシュデコレーター
class CachingUserService implements UserServiceInterface
{
    private array $cache = [];

    public function __construct(
        private readonly UserServiceInterface $inner,
        private readonly int $ttl = 300,
    ) {}

    public function findById(int $id): ?array
    {
        if (isset($this->cache[$id])) {
            echo "[CACHE] ユーザー {$id} をキャッシュから返します\n";
            return $this->cache[$id];
        }

        $result = $this->inner->findById($id);
        $this->cache[$id] = $result;
        return $result;
    }

    public function create(array $data): array
    {
        $result = $this->inner->create($data);
        // 作成後にキャッシュを更新
        $this->cache[$result['id']] = $result;
        return $result;
    }
}

デコレーターは玉ねぎのように重ねられます

// 単独で使う
$service = new UserService();

// ログ機能を追加
$service = new LoggingUserService(new UserService());

// ログ + キャッシュを追加(順序はデコレーターを重ねる順で決まる)
$service = new LoggingUserService(
    new CachingUserService(
        new UserService()
    )
);

// 同じインターフェースで使えるため呼び出し側は変更不要
$user = $service->findById(1);
// [LOG] findById(1) を呼び出しました
// DB: ユーザー 1 を取得
// [LOG] findById(1) が完了しました

$user = $service->findById(1); // 2回目
// [LOG] findById(1) を呼び出しました
// [CACHE] ユーザー 1 をキャッシュから返します
// [LOG] findById(1) が完了しました

ミドルウェアはDecoratorパターン

HTTPミドルウェアはDecoratorパターンそのものです。リクエストとレスポンスを処理するパイプラインを「積み重ねたデコレーター」で実現しています。

// PSR-15スタイルのミドルウェア(Decoratorと同じ構造)
interface Middleware
{
    public function handle(Request $request, callable $next): Response;
}

class AuthMiddleware implements Middleware
{
    public function handle(Request $request, callable $next): Response
    {
        if (!$request->hasHeader('Authorization')) {
            return new Response(401, 'Unauthorized');
        }
        return $next($request); // 次のミドルウェアへ委譲
    }
}

class LoggingMiddleware implements Middleware
{
    public function handle(Request $request, callable $next): Response
    {
        echo "[LOG] {$request->method} {$request->path}\n";
        $response = $next($request);
        echo "[LOG] レスポンス: {$response->status}\n";
        return $response;
    }
}

Laravelのミドルウェアとの対応

Laravelのミドルウェアは $next($request) を呼ぶことで次の層に処理を渡します。これはまさにDecoratorパターンのチェーン呼び出しです。

// app/Http/Middleware/EnsureApiKey.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class EnsureApiKey
{
    public function handle(Request $request, Closure $next): mixed
    {
        // 前処理(リクエストのデコレーション)
        if ($request->header('X-API-KEY') !== config('app.api_key')) {
            return response()->json(['error' => 'Invalid API key'], 401);
        }

        // 次の層(コントローラーまたは次のミドルウェア)に委譲
        $response = $next($request);

        // 後処理(レスポンスのデコレーション)
        $response->headers->set('X-Processed-By', 'MyApp');

        return $response;
    }
}

ルートへの適用は「デコレーターを重ねる」イメージと同じです。

Route::middleware(['auth', 'log', 'throttle:60,1'])
    ->prefix('api')
    ->group(function () {
        Route::get('/users', [UserController::class, 'index']);
    });

ストリームへの応用

PHPのストリームラッパーもDecoratorパターンを使っています。

// 暗号化 + 圧縮デコレーターのイメージ
interface DataStream
{
    public function write(string $data): void;
    public function read(): string;
}

class FileStream implements DataStream
{
    private string $content = '';

    public function write(string $data): void { $this->content .= $data; }
    public function read(): string { return $this->content; }
}

class GzipStream implements DataStream
{
    public function __construct(private readonly DataStream $inner) {}

    public function write(string $data): void
    {
        $this->inner->write(gzencode($data));
    }

    public function read(): string
    {
        return gzdecode($this->inner->read());
    }
}

まとめ

  • Decoratorパターンは同じインターフェースを実装したラッパーで機能を追加する
  • 継承の「クラス爆発」問題を避けられる。組み合わせは実行時に自由に決められる
  • ミドルウェアはDecoratorパターンの典型例($next($request) がチェーン呼び出し)
  • ログ出力・キャッシュ・認証チェックなど「横断的な関心事」の実装に最適

次回はRepositoryパターンで、データアクセスをビジネスロジックから分離する方法を解説します。