#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パターンで、継承に頼らず機能を動的に追加する方法を解説します。