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

Factory Method——オブジェクト生成を子クラスに任せる

問題: 種類が増えるたびにコードを修正しなければならない

通知機能を実装するとき、最初は「メールだけ」でよかったのに、あとから「SMSも送りたい」「プッシュ通知も追加したい」と要件が増えることがあります。

// 問題のあるコード
class NotificationService
{
    public function send(string $type, string $message): void
    {
        if ($type === 'email') {
            $notifier = new EmailNotifier();
        } elseif ($type === 'sms') {
            $notifier = new SmsNotifier();
        } elseif ($type === 'push') {
            $notifier = new PushNotifier(); // 追加するたびにここを修正
        }

        $notifier->send($message);
    }
}

種類が増えるたびに if/elseif を追加する必要があり、Open/Closed Principle(拡張に開き、修正に閉じる)に違反します。


パターン: Factory Method

Factory Methodパターンは、オブジェクトの生成を専用のメソッド(ファクトリメソッド)に委ね、そのメソッドを子クラスがオーバーライドして具体的な型を決めるパターンです。

+-----------------------------+
|     Notifier <<interface>>  |
+-----------------------------+
| + send(message: string)     |
+-----------------------------+

         |
+--------+--------+----------+
|                 |          |
EmailNotifier  SmsNotifier  PushNotifier


+-----------------------------+
|   NotifierFactory (abstract)|
+-----------------------------+
| + createNotifier(): Notifier|  ← ファクトリメソッド(抽象)
| + notify(message: string)   |
+-----------------------------+

         |
+--------+--------+-----------+
|                 |           |
EmailFactory   SmsFactory   PushFactory

PHP 8.x での実装

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

<?php

// 通知の共通インターフェース
interface Notifier
{
    public function send(string $message): void;
}

// Email通知
class EmailNotifier implements Notifier
{
    public function __construct(private readonly string $to) {}

    public function send(string $message): void
    {
        echo "Sending email to {$this->to}: {$message}\n";
        // 実際のメール送信処理
    }
}

// SMS通知
class SmsNotifier implements Notifier
{
    public function __construct(private readonly string $phoneNumber) {}

    public function send(string $message): void
    {
        echo "Sending SMS to {$this->phoneNumber}: {$message}\n";
        // 実際のSMS送信処理
    }
}

// プッシュ通知
class PushNotifier implements Notifier
{
    public function __construct(private readonly string $deviceToken) {}

    public function send(string $message): void
    {
        echo "Sending push to {$this->deviceToken}: {$message}\n";
        // 実際のプッシュ通知処理
    }
}

次にファクトリの抽象クラスと具体的なファクトリを実装します。

<?php

// 抽象ファクトリクラス
abstract class NotifierFactory
{
    // ファクトリメソッド: 子クラスがオーバーライドして具体型を返す
    abstract protected function createNotifier(): Notifier;

    // テンプレートとなるメソッド: 生成と利用をセットにする
    public function notify(string $message): void
    {
        $notifier = $this->createNotifier();
        $notifier->send($message);
    }
}

// Email用ファクトリ
class EmailNotifierFactory extends NotifierFactory
{
    public function __construct(private readonly string $email) {}

    protected function createNotifier(): Notifier
    {
        return new EmailNotifier($this->email);
    }
}

// SMS用ファクトリ
class SmsNotifierFactory extends NotifierFactory
{
    public function __construct(private readonly string $phone) {}

    protected function createNotifier(): Notifier
    {
        return new SmsNotifier($this->phone);
    }
}

// Push用ファクトリ
class PushNotifierFactory extends NotifierFactory
{
    public function __construct(private readonly string $token) {}

    protected function createNotifier(): Notifier
    {
        return new PushNotifier($this->token);
    }
}

使う側のコードはこうなります。

// 使用例
function sendWelcomeNotification(NotifierFactory $factory): void
{
    $factory->notify("ようこそ!アカウントが作成されました。");
}

// 通知方法をファクトリで切り替えるだけ
sendWelcomeNotification(new EmailNotifierFactory('user@example.com'));
sendWelcomeNotification(new SmsNotifierFactory('090-1234-5678'));
sendWelcomeNotification(new PushNotifierFactory('device_token_abc'));

新しい通知方法(例: Slack)を追加するときは、SlackNotifierSlackNotifierFactory を追加するだけで、既存のコードは一切変更不要です。


Abstract Factory との違い

よく混同される Abstract Factory との違いを整理しておきましょう。

比較項目Factory MethodAbstract Factory
目的ひとつの製品の生成を委譲関連する複数の製品群をまとめて生成
単位メソッド単位クラス単位
Notifier を作るUI部品一式(ボタン・フォーム・ダイアログ)をまとめて作る
// Abstract Factoryのイメージ
interface UiFactory
{
    public function createButton(): Button;
    public function createInput(): Input;
    public function createDialog(): Dialog;
}

class DarkThemeFactory implements UiFactory
{
    public function createButton(): Button { return new DarkButton(); }
    public function createInput(): Input   { return new DarkInput(); }
    public function createDialog(): Dialog { return new DarkDialog(); }
}

Laravelでのファクトリ活用例

Laravelのモデルファクトリdatabase/factories)もFactory Methodパターンの考え方を採用しています。テスト用のデータ生成をファクトリクラスに集約します。

// database/factories/UserFactory.php

namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

class UserFactory extends Factory
{
    protected $model = User::class;

    // definition() がファクトリメソッドに相当
    public function definition(): array
    {
        return [
            'name'  => fake()->name(),
            'email' => fake()->unique()->safeEmail(),
            'role'  => 'user',
        ];
    }

    // 状態メソッドで派生型を定義
    public function admin(): static
    {
        return $this->state(['role' => 'admin']);
    }
}

// テストコード
$user  = User::factory()->create();         // 通常ユーザー
$admin = User::factory()->admin()->create(); // 管理者ユーザー

また、通知チャンネルの選択にも同様の考え方が使えます。

// app/Notifications/UserRegistered.php

class UserRegistered extends Notification
{
    public function via(object $notifiable): array
    {
        // ユーザーの設定に応じてチャンネルを切り替え
        return match ($notifiable->notification_preference) {
            'sms'  => ['nexmo'],
            'push' => ['fcm'],
            default => ['mail'],
        };
    }
}

まとめ

  • Factory Methodは「どのクラスを生成するか」を子クラスに委ねるパターン
  • if/elseif による生成分岐をなくし、拡張に強い設計になる
  • Abstract Factoryは複数の関連オブジェクトをまとめて生成するパターン
  • Laravelのモデルファクトリ・通知チャンネルでも同じ考え方が使われている

次回はBuilderパターンで、複雑なオブジェクトを段階的に組み立てる方法を解説します。