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

まとめ——パターンを活かした設計の考え方

シリーズを振り返って

このシリーズでは、実務でよく使われる10のデザインパターンをPHP 8.xのコードとLaravelの例で解説してきました。

パターンひとことまとめ
02Singletonインスタンスをひとつに制限
03Factory Method生成を子クラスに委ねる
04Builder複雑な生成をメソッドチェーンで
05Strategyアルゴリズムを実行時に差し替え
06Observer状態変化をObserverに自動通知
07Decoratorオブジェクトをラップして機能追加
08Repositoryデータアクセスをインターフェースで隠蔽
09Command操作をオブジェクト化してキュー・Undo
10Adapter互換性のないクラスを統一インターフェースで
11Template 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: シンプルな関数や直接のメソッド呼び出しで解決できる場面です。パターンは問題を解決するためのツールであり、目的ではありません。


次のステップ

このシリーズで基礎を学んだ後は、以下の学習を推奨します。

  1. SOLID原則の深掘り — パターンの「なぜ」をより深く理解できる
  2. ドメイン駆動設計(DDD) — Repository・Value Object・Aggregateを実務規模で使う
  3. リファクタリング(Martin Fowler) — コードの臭いとリファクタリング手法の体系
  4. テスト駆動開発(TDD) — パターンと組み合わせることで設計品質が上がる

まとめ

  • パターンは「問題ありき」で選ぶ。目的ではなく手段
  • 過剰設計を避ける(YAGNI)。シンプルな解決が最優先
  • SOLID原則とパターンは表裏一体。原則を守るとパターンが自然に現れる
  • リファクタリングは小さく段階的に。テストをセーフティネットにする
  • フレームワーク(Laravel)のソースコードを読むとパターンの使用例が豊富に見つかる

全12回にわたる「デザインパターン入門」シリーズを読んでいただきありがとうございました。パターンを武器に、より保守性の高いコードを書いていきましょう。