まとめ——パターンを活かした設計の考え方
シリーズを振り返って
このシリーズでは、実務でよく使われる10のデザインパターンをPHP 8.xのコードとLaravelの例で解説してきました。
| 回 | パターン | ひとことまとめ |
|---|---|---|
| 02 | Singleton | インスタンスをひとつに制限 |
| 03 | Factory Method | 生成を子クラスに委ねる |
| 04 | Builder | 複雑な生成をメソッドチェーンで |
| 05 | Strategy | アルゴリズムを実行時に差し替え |
| 06 | Observer | 状態変化をObserverに自動通知 |
| 07 | Decorator | オブジェクトをラップして機能追加 |
| 08 | Repository | データアクセスをインターフェースで隠蔽 |
| 09 | Command | 操作をオブジェクト化してキュー・Undo |
| 10 | Adapter | 互換性のないクラスを統一インターフェースで |
| 11 | Template Method | 骨格を抽象クラスで定義 |
パターンの選び方
パターンを選ぶときは「このパターンを使いたい」ではなく、「この問題を解決するのに適したパターンは何か」から考えます。
問題から逆引きする
問題: 同じ処理が if/elseif で増え続けている
→ Strategy(アルゴリズムの差し替え)
→ Factory Method(生成の差し替え)
問題: 変更のたびに多くのクラスを修正する必要がある
→ Observer(変更の通知を疎結合に)
→ Repository(DBアクセスをインターフェースで隠蔽)
問題: 外部ライブラリへの依存が直接コードに入り込んでいる
→ Adapter(互換インターフェースでラップ)
問題: 機能の組み合わせでクラスが爆発している
→ Decorator(ラップで機能追加)
問題: 操作を記録・取り消し・非同期実行したい
→ Command(操作のオブジェクト化)
問題: 手順は同じなのに実装ごとにコピーが増えている
→ Template Method(骨格を抽象クラスに)
小さくはじめる
初めてパターンを使うときは、小さな範囲から試すのが大切です。いきなりシステム全体にRepositoryを導入するのではなく、「テストが難しい箇所だけ」から始めます。
過剰設計に注意
デザインパターンを学んだばかりのころ、何にでもパターンを適用したくなることがあります。これは「過剰設計(Over-Engineering)」の落とし穴です。
// やりすぎの例: 単純なユーザー名のフォーマットに Strategy を適用
interface NameFormatterStrategy
{
public function format(string $firstName, string $lastName): string;
}
class JapaneseNameFormatter implements NameFormatterStrategy
{
public function format(string $firstName, string $lastName): string
{
return "{$lastName} {$firstName}";
}
}
// ← これが必要な場面はほぼない。ただの関数で十分
// シンプルな解決:
function formatName(string $firstName, string $lastName): string
{
return "{$lastName} {$firstName}";
}
YAGNIの原則(You Ain’t Gonna Need It):今必要でない機能・抽象化は今作らない。
パターンを導入する前に問いかけましょう:
- この抽象化は今すでに必要か?
- 将来の変更が本当に予測できるか?
- シンプルなコードで解決できないか?
SOLID原則とパターンの関係
デザインパターンはSOLID原則と深く結びついています。
S: 単一責任原則(Single Responsibility Principle)
各クラスは1つの責任だけを持つ。
// Repositoryパターンでデータアクセスを分離
// OrderService → 注文のビジネスロジックだけ担当
// OrderRepository → DBアクセスだけ担当
O: 開放閉鎖原則(Open/Closed Principle)
拡張に開き、修正に閉じる。
// Strategyパターンで新しい支払い方法を追加
// → PaymentService は変更せず、新しいStrategyクラスを追加するだけ
// Factory Methodパターンで新しい通知方法を追加
// → NotifierFactory は変更せず、新しいFactoryクラスを追加するだけ
L: リスコフの置換原則(Liskov Substitution Principle)
子クラスは親クラスの代わりに使えなければならない。
// AdapterパターンとRepositoryパターン
// → どの実装クラスも同じインターフェースで差し替え可能
$gateway = new StripeAdapter(...); // PaymentGateway として使える
$gateway = new PayPalAdapter(...); // こちらも同じ PaymentGateway
I: インターフェース分離原則(Interface Segregation Principle)
大きなインターフェースより小さなインターフェースを複数作る。
// 悪い例: なんでも入ったインターフェース
interface UserRepository
{
public function find(int $id): User;
public function save(User $user): void;
public function generateReport(): string; // ← 責任が混在
public function sendNotification(): void; // ← これも混在
}
// 良い例: 役割ごとに分割
interface UserReadRepository { public function find(int $id): User; }
interface UserWriteRepository { public function save(User $user): void; }
D: 依存性逆転の原則(Dependency Inversion Principle)
具体クラスではなく抽象(インターフェース)に依存する。
// 具体クラスへの依存(変更に弱い)
class OrderService
{
private EloquentUserRepository $repo; // 具体クラス
}
// インターフェースへの依存(変更に強い)
class OrderService
{
private UserRepository $repo; // インターフェース ← こちらが正しい
}
リファクタリングでパターンを取り入れる手順
既存コードにパターンを取り入れるときは、段階的に行うのが安全です。
ステップ 1: テストを書く
まず既存の動作をテストでカバーします。リファクタリング後も同じ動作をすることを確認するためです。
// リファクタリング前のテスト
public function testPaymentProcessing(): void
{
$service = new PaymentService();
$result = $service->pay('credit_card', 5800);
$this->assertTrue($result);
}
ステップ 2: コードの「臭い(Code Smell)」を特定する
- 長い if/elseif 連鎖 → Strategy、Factory Method
- クラスの肥大化 → Strategy、Observer、Repository
- コードの重複 → Template Method、Builder
- 外部依存が散在 → Adapter、Repository
ステップ 3: インターフェースを抽出する
// Before: 直接 Stripe を使っている
class CheckoutService
{
public function charge(int $amount): void
{
$stripe = new \Stripe\StripeClient(env('STRIPE_KEY'));
$stripe->paymentIntents->create(['amount' => $amount]);
}
}
// Step 1: インターフェースを作る
interface PaymentGateway
{
public function charge(int $amount, string $orderId): string;
}
// Step 2: 既存コードをAdapterで包む
class StripeAdapter implements PaymentGateway { ... }
// Step 3: CheckoutService をインターフェースに依存させる
class CheckoutService
{
public function __construct(private readonly PaymentGateway $gateway) {}
public function charge(int $amount, string $orderId): void
{
$this->gateway->charge($amount, $orderId);
}
}
ステップ 4: テストで動作を確認する
リファクタリング後もステップ1で書いたテストがすべて通ることを確認します。
ステップ 5: 小さくコミットする
1度のコミットに大きな変更を詰め込まず、「インターフェース追加」「Adapter実装」「CheckoutService変更」と段階的にコミットします。
よくある質問
Q: パターンは全部覚えないといけないか?
A: いいえ。まずはStrategy・Observer・Repository・Decoratorの4つを実務で使えるようになることを目指しましょう。残りは必要に応じて調べれば十分です。
Q: フレームワークを使っているのにパターンを学ぶ意味はあるか?
A: あります。Laravelのジョブ(Command)、イベントシステム(Observer)、Storage(Adapter)、Eloquentビルダー(Builder)はすべてパターンの実装です。パターンを知っているとフレームワークの設計意図がわかり、正しく使えるようになります。
Q: いつパターンを使わないべきか?
A: シンプルな関数や直接のメソッド呼び出しで解決できる場面です。パターンは問題を解決するためのツールであり、目的ではありません。
次のステップ
このシリーズで基礎を学んだ後は、以下の学習を推奨します。
- SOLID原則の深掘り — パターンの「なぜ」をより深く理解できる
- ドメイン駆動設計(DDD) — Repository・Value Object・Aggregateを実務規模で使う
- リファクタリング(Martin Fowler) — コードの臭いとリファクタリング手法の体系
- テスト駆動開発(TDD) — パターンと組み合わせることで設計品質が上がる
まとめ
- パターンは「問題ありき」で選ぶ。目的ではなく手段
- 過剰設計を避ける(YAGNI)。シンプルな解決が最優先
- SOLID原則とパターンは表裏一体。原則を守るとパターンが自然に現れる
- リファクタリングは小さく段階的に。テストをセーフティネットにする
- フレームワーク(Laravel)のソースコードを読むとパターンの使用例が豊富に見つかる
全12回にわたる「デザインパターン入門」シリーズを読んでいただきありがとうございました。パターンを武器に、より保守性の高いコードを書いていきましょう。