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

Observer——イベントと通知を疎結合にする

問題: 処理が増えるたびにメソッドが肥大化する

ユーザー登録が完了したとき、「ウェルカムメールを送る」「管理者にSlack通知する」「ポイントを付与する」という処理が必要です。これらを全部 register() メソッドに書いていくとどうなるでしょうか。

// 問題のあるコード: 処理が増えるたびに UserService を修正する
class UserService
{
    public function register(array $data): User
    {
        $user = User::create($data);

        // ウェルカムメールを送る
        $this->mailer->send($user->email, 'welcome');

        // 管理者にSlack通知
        $this->slack->notify("#admin", "新規登録: {$user->name}");

        // ポイント付与
        $this->pointService->grant($user->id, 100);

        // アナリティクスに記録(あとで追加)
        $this->analytics->track('user_registered', $user->id);

        return $user;
    }
}

登録完了後の処理が増えるたびに UserService を修正する必要があり、依存が増え続けます。


パターン: Observer

Observerパターンは、オブジェクトの状態変化を「購読者(Observer)」に自動で通知するパターンです。Pub/Sub(Publisher-Subscriber)パターンとも呼ばれます。

+------------------------------+
|   Subject (Observable)       |
+------------------------------+
| - observers: array           |
+------------------------------+
| + attach(observer): void     |
| + detach(observer): void     |
| + notify(): void             |
+------------------------------+
              |
              | 通知

+------------------------------+
|   Observer <<interface>>     |
+------------------------------+
| + update(subject): void      |
+------------------------------+

    _____|_____
   |           |
WelcomeMail  SlackNotify  PointGrant
Observer     Observer     Observer

PHPのSplObserverを使った実装

PHPには標準ライブラリとして SplObserver / SplSubject インターフェースが用意されています。

<?php

// Subject(イベントを発生させる側)
class UserRegistration implements \SplSubject
{
    private \SplObjectStorage $observers;
    private ?User $registeredUser = null;

    public function __construct()
    {
        $this->observers = new \SplObjectStorage();
    }

    public function attach(\SplObserver $observer): void
    {
        $this->observers->attach($observer);
    }

    public function detach(\SplObserver $observer): void
    {
        $this->observers->detach($observer);
    }

    public function notify(): void
    {
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }

    public function getUser(): ?User
    {
        return $this->registeredUser;
    }

    public function register(array $data): User
    {
        // ユーザーを作成
        $this->registeredUser = new User($data);

        // 登録した全Observerに通知
        $this->notify();

        return $this->registeredUser;
    }
}

// Observer(通知を受け取る側)
class WelcomeMailObserver implements \SplObserver
{
    public function update(\SplSubject $subject): void
    {
        $user = $subject->getUser();
        echo "ウェルカムメールを {$user->email} に送信しました\n";
    }
}

class SlackNotifyObserver implements \SplObserver
{
    public function update(\SplSubject $subject): void
    {
        $user = $subject->getUser();
        echo "Slack #admin に通知: 新規登録 {$user->name}\n";
    }
}

class PointGrantObserver implements \SplObserver
{
    public function update(\SplSubject $subject): void
    {
        $user = $subject->getUser();
        echo "{$user->name} に100ポイントを付与しました\n";
    }
}

使う側では、どのObserverを購読させるかを自由に設定できます。

$registration = new UserRegistration();

// Observerを登録(いくつでも追加可能)
$registration->attach(new WelcomeMailObserver());
$registration->attach(new SlackNotifyObserver());
$registration->attach(new PointGrantObserver());

// ユーザー登録 → 自動でObserverに通知される
$user = $registration->register([
    'name'  => 'Alice',
    'email' => 'alice@example.com',
]);

// UserService はもうWelcomeMailやSlackについて知らなくてよい

新しい処理(例: アナリティクス記録)を追加したいときは、AnalyticsObserver を実装して attach() するだけです。UserRegistration も既存のObserverも変更不要です。


イベント駆動設計への発展

ObserverパターンをさらにシンプルにしたのがPHP向けの軽量イベントディスパッチャーです。

<?php

// シンプルなイベントディスパッチャー
class EventDispatcher
{
    private array $listeners = [];

    public function on(string $event, callable $listener): void
    {
        $this->listeners[$event][] = $listener;
    }

    public function dispatch(string $event, mixed $payload = null): void
    {
        foreach ($this->listeners[$event] ?? [] as $listener) {
            $listener($payload);
        }
    }
}

// イベントクラス
class UserRegistered
{
    public function __construct(
        public readonly string $name,
        public readonly string $email,
        public readonly int $id,
    ) {}
}

// セットアップ
$dispatcher = new EventDispatcher();

$dispatcher->on('user.registered', function (UserRegistered $event) {
    echo "メール送信: {$event->email}\n";
});

$dispatcher->on('user.registered', function (UserRegistered $event) {
    echo "ポイント付与: {$event->id}\n";
});

// イベント発火
$dispatcher->dispatch('user.registered', new UserRegistered('Alice', 'alice@example.com', 1));

Laravelのイベント/リスナーシステムとの対応

Laravelはイベント/リスナーシステムを標準搭載しており、Observerパターンをより強力にした実装を提供しています。

// イベントクラス: app/Events/UserRegistered.php
namespace App\Events;

use App\Models\User;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class UserRegistered
{
    use Dispatchable, SerializesModels;

    public function __construct(
        public readonly User $user,
    ) {}
}
// リスナークラス: app/Listeners/SendWelcomeMail.php
namespace App\Listeners;

use App\Events\UserRegistered;

class SendWelcomeMail
{
    public function handle(UserRegistered $event): void
    {
        // ウェルカムメールを送信
        Mail::to($event->user->email)->send(new WelcomeMailable($event->user));
    }
}
// EventServiceProvider でリスナーを登録
protected $listen = [
    UserRegistered::class => [
        SendWelcomeMail::class,
        NotifyAdminViaSlack::class,
        GrantWelcomePoints::class,
    ],
];
// UserService はイベントを発火するだけ
class UserService
{
    public function register(array $data): User
    {
        $user = User::create($data);

        // イベント発火 → リスナーが自動で実行される
        UserRegistered::dispatch($user);

        return $user;
    }
}

LaravelのEloquentモデルにも Observing 機能があり、モデルイベント(created, updated, deleted等)を購読できます。

// app/Observers/UserObserver.php
class UserObserver
{
    public function created(User $user): void
    {
        GrantWelcomePoints::dispatch($user);
    }

    public function deleted(User $user): void
    {
        CleanupUserData::dispatch($user);
    }
}

// AppServiceProvider に登録
User::observe(UserObserver::class);

まとめ

  • Observerパターンはオブジェクトの状態変化を購読者(Observer)に自動通知する
  • SubjectはObserverの具体的な処理を知らなくてよい(疎結合)
  • 処理を追加するときは新しいObserverを作って登録するだけ
  • LaravelのイベントシステムはObserverパターンを非同期キューとも統合した強力な実装

次回はDecoratorパターンで、継承に頼らず機能を動的に追加する方法を解説します。