#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パターンで、データアクセスをビジネスロジックから分離する方法を解説します。