Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ドメインイベントでビジネスロジックを解きほぐす #phpcon_odawara

ドメインイベントでビジネスロジックを解きほぐす #phpcon_odawara

PHPカンファレンス小田原2026の登壇資料です。

ドメインイベントは「〇〇が起きた」という事実をクラス化し、処理の流れを“出来事”をベースに組み立て直すことで、依存を一方向に整えることができます。

本トークでは実務でのリファクタリングを題材に、

- 従来のメソッド分割との違いや限界を体感
- 同期イベント導入
- 非同期化
の3ステップでリファクタリング前と後のコードで比較しながら順に解説します。
チームでドメインイベントをわいわい議論できるようになるきっかけを提供します!

https://fortee.jp/phpconodawara-2026/proposal/6ff7ef63-7a95-4482-bb24-0a1a4f5bac96

Avatar for Takuma Kajikawa

Takuma Kajikawa

April 10, 2026

More Decks by Takuma Kajikawa

Other Decks in Programming

Transcript

  1. 梶川 琢馬 𝕏 @kajitack 株式会社 TechBowl VPoT TechTrain の開発やメンターを担当してます! 関数型まつりコアスタッフ

    コミュニティかわらばんに出展してます! X でスライド公開してます! https://x.com/kajitack 2/36
  2. ユーザー登録のよくあるコード 適切にメソッドが分割されていて、見通しの良いコード? メール送信の仕様が変わるだけで、ユーザー登録全体のテストが壊れる class UserController { public function create($userData) {

    $user = new User($userData['email'], $userData['name']); $this->userRepository->save($user); $this->mailService->sendWelcomeEmail($user); // メール送信 $this->notificationService->notifyAdmin($user); // 運営に通知 $this->planService->createFreePlan($user); // 契約プラン作成 $this->externalService->syncUser($user); // 外部連携 } } 6/36
  3. 結合度(Coupling) コンポーネント間の依存関係の強さ。結合度が高いと変更が連鎖する 疎結合にすれば、変更の影響をそのコンポーネント内に閉じ込められる 密結合(⾼結合) コンポーネント同⼠が互いを直接知っている 疎結合(低結合) コンポーネント同⼠が互いを知らない コンポーネントA ▲ 変更が発⽣

    コンポーネントB コンポーネントC コンポーネントD A の変更 → B, C, D すべてに影響 修正・テストが連鎖的に必要になる コンポーネントA ▲ 変更が発⽣ コンポーネントB コンポーネントC インターフェース / イベント A の変更 → C, B には影響しない インターフェース経由で独⽴を保つ 9/36
  4. 結合度の2つの軸: 振る舞い × 時間 メソッド分割では右上のまま。ドメインイベントで左上→左下へ段階的に移動できる 振る舞い的結合(統合強度) 時間的結合 コントラクト結合 事実の通知 機能結合

    振る舞いの指⽰ 低 ⾼ 機能結合 ̶ 呼び出し側が仕事を知っている 巨⼤メソッド メソッド分割 サービス分割 Step 0 分割しても 結合の種類は変わらない コントラクト結合 ̶ 事実だけを共有する 同期イベント 振る舞い的結合を解消 時間的結合は残る ⾮同期イベント 両⽅の結合を解消 Step 1 機能結合 → コントラクト結合 Step 2 同期 → ⾮同期 10/36
  5. 「振る舞い」を指示するか、「事実」を通知するか 「コマンド(これから実行すべき操作を表現したメッセージ)」 「イベント(すでに起こった変化を表現した事実)」 コマンド型 「メールを送れ」と指⽰ UserController sendEmail() notifyAdmin() createPlan() 送信側が受信側の仕事を知っている

    振る舞い的結合が残る イベント型 「ユーザ���が作成された」と通知 UserController UserCreatedEvent MailSubscriber NotifySubscriber PlanSubscriber 送信側は受信側の存在を知らない 振る舞い的結合を断ち切れる 11/36
  6. Publisher と Subscriber Publisher は「何が起きたか」を発行するだけ Subscriber は関心のあるイベントだけを受け取る Publisher UserCreateUsecase イベントを発⾏する

    publish Event UserCreatedEvent 「何が起きたか」の通知 Subscriber handle MailSubscriber NotifySubscriber PlanSubscriber Publisher → Subscriber の 直接のつながりはない Publisher と Subscriber はイベントだけを共有し、 互いの存在を知らない 14/36
  7. Before: 副作用の数だけモックが必要 #[Test] public function ユーザーを作成する() { $repo = $this->mock(UserRepository::class);

    $mail = $this->mock(MailService::class); $notification = $this->mock(NotificationService::class); $plan = $this->mock(PlanService::class); $external = $this->mock(ExternalService::class); // 全てのモックに期待値設定... $repo->expects($this->once())->method('save'); $mail->expects($this->once())->method('sendWelcomeEmail'); // ...副作用が増えるたびにモックも増える } 16/36
  8. Step 1: ドメインイベントで 振る舞い的結合を解消 同期処理のまま、依存の方向を逆転させる 振る舞い的結合(統合強度) 時間的結合 コントラクト結合 事実の通知 機能結合

    振る舞いの指⽰ 低 ⾼ 機能結合 ̶ 呼び出し側が仕事を知っている 巨⼤メソッド メソッド分割 サービス分割 Step 0 分割しても 結合の種類は変わらない コントラクト結合 ̶ 事実だけを共有する 同期イベント 振る舞い的結合を解消 時間的結合は残る ⾮同期イベント 両⽅の結合を解消 Step 1 機能結合 → コントラクト結合 Step 2 同期 → ⾮同期 20/36
  9. Subscriber Publisher Repository Domain Model Usecase Subscriber Publisher Repository Domain

    Model Usecase インスタンス作成 イベント作成 Publisher の登録 永続化処理 ドメインイベントを取得 イベントの発⾏ イベントを受け取る ドメインイベントの流れ 1. ドメインモデルがイベントを生成・保持する 2. 永続化した後にイベントを 取り出し、Publisher で発行する 3. Subscriber がイベントに反応して処理を実行する 同期/非同期の違いは Subscriber の実行タイミング 21/36
  10. Before: 全ての副作用を直接呼び出す Usecase が 3 つのサービスに依存。1 つ変わればテストも修正が必要 class UserCreateUsecase {

    public function exec(UserId $userId, Email $email): void { $user = User::create($userId, $email); DB::transaction(function () use ($user) { $this->userRepository->save($user); }); // 副作用を全て知っている $this->mailService->sendWelcomeMail($user->email()); $this->notificationService->notifyAdmin($user); $this->planService->createFreePlan($user->id()); } } 22/36
  11. After: イベントを発行するだけ Usecase は副作用を知らない。イベントへ反応する Subscriber が独立に処理する class UserCreateUsecase { public

    function exec(UserId $userId, Email $email): void { $user = User::create($userId, $email); DB::transaction(function () use ($user) { $this->userRepository->save($user); // 永続化後にイベントを発行するだけ foreach ($user->pullDomainEvents() as $event) { $this->eventPublisher->publish($event); } }); } } 23/36
  12. Subscriber: イベントに反応する独立したクラス お互いに疎結合で、ドメインイベントにのみ依存 class WelcomeMailSubscriber { public function handle(UserCreatedEvent $event):

    void { $this->mailService->sendWelcomeMail($event->email()); } } class CreatePlanSubscriber { public function handle(UserCreatedEvent $event): void { $this->planService->createFreePlan($event->userId()); } } 24/36
  13. 同期処理だけで テスト・変更・依存が改善する Before テスト: モック N 個 変更: 副作用追加で Usecase

    を修正 依存: Usecase が全サービスに依存 After テスト: モック 1 個 変更: Subscriber を追加するだけ 依存: EventPublisher のみに依存 25/36
  14. Step 2: Subscriber の実行タイミングを変える ユーザーはイベント発行後すぐにレスポンスを受け取れる → 時間的結合の解消 振る舞い的結合(統合強度) 時間的結合 コントラクト結合

    事実の通知 機能結合 振る舞いの指⽰ 低 ⾼ 機能結合 ̶ 呼び出し側が仕事を知っている 巨⼤メソッド メソッド分割 サービス分割 Step 0 分割しても 結合の種類は変わらない コントラクト結合 ̶ 事実だけを共有する 同期イベント 振る舞い的結合を解消 時間的結合は残る ⾮同期イベント 両⽅の結合を解消 Step 1 機能結合 → コントラクト結合 Step 2 同期 → ⾮同期 26/36
  15. 非同期化の手段: メッセージングパターン Publisher が受信者を知らない構造なので Pub/Sub パターンが適している Point-to-Point Queue 1 対

    1 で確実に届ける Producer Queue Consumer 特定の処理に確実に届けたい場合 例: ジョブキュー、タスク分散 Pub/Sub 1 対多でイベントを配信 Publisher Topic Sub A Sub B Sub C ドメインイベントの配信に最適 例: ユーザー作成 → メール、通知、プラン 27/36
  16. Outbox パターンのコード例 別プロセス(バッチ/ワーカー)が未処理イベントを取り出して配信する 同じトランザクションに入れるだけで、DB コミットとイベント配信の整合性を保証できる class UserRepository { public function

    save(User $user): void { DB::transaction(function () use ($user) { $this->dao->save($user); // 同じトランザクションでイベントも保存 foreach ($user->pullDomainEvents() as $event) { $this->eventStore->save($event); // イベントテーブルにINSERT } }); } } 30/36
  17. DBに保存したイベントをバッチ処理でpublish Subscriber Pub/Sub Batch Event Store (DB) Subscriber Pub/Sub Batch

    Event Store (DB) 未処理イベントを取得 (status = 'pending') UserCreatedEvent のリスト メッセージを送信 (Topic: user-events) メッセージを送信 (Topic: user-events) イベントを処理済みにマーク (status = 'processed') 31/36
  18. 段階的に進める 同期処理だけでも十分価値がある 非同期化は必要になってから Step 1: 同期処理 振る舞い的結合を解消 導入コストが低い すぐに始められる Step

    2: 非同期化 時間的結合も解消 パフォーマンス改善 注意: 整合性→Outbox、重複→冪等性、順序→正しい イベントへの反応 33/36
  19. ドメインイベントで ビジネスロジックを解きほぐす ビジネスロジックを「結合」の観点から見直し、 「ドメインイベント」で段階的に整理する ドメインイベント:「事実の通知」で依存の方向を逆転。各 Subscriber が独立して動き、追加は Subscriber を足すだけ。 変更も

    Subscriber に閉じ、テストのモックも激減する。 ドメインイベントの非同期化: 重い処理の時間的な依存を 解消する。整合性・重複・順序の課題には Outbox・冪等性・ 順序非依存、補償トランザクションで対処。ドメインイベントを 同期的に処理するだけでも振る舞いの結合は解消されるので、 段階的に進める。 34/36
  20. 参考資料 「実践ドメイン駆動設計」Vaughn Vernon 著 / 髙木正弘訳 「ドメイン駆動設計をはじめよう」Vlad Khononov 著 /

    増田亨監訳 / 黒田樹訳 「ソフトウェア設計の結合バランス」Vlad Khononov 著 / 島田浩二訳 35/36