Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
ドメインイベントでビジネスロジックを解きほぐす #phpcon_odawara
Search
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
Takuma Kajikawa
April 10, 2026
Programming
8
0
Share
ドメインイベントでビジネスロジックを解きほぐす #phpcon_odawara
PHPカンファレンス小田原2026の登壇資料です。
Takuma Kajikawa
April 10, 2026
More Decks by Takuma Kajikawa
See All by Takuma Kajikawa
実践ハーネスエンジニアリング #MOSHTech
kajitack
7
5.5k
メッセージングを利用して時間的結合を分離しよう #phperkaigi
kajitack
3
550
TechTrain開発 x AI #CircLeT
kajitack
0
71
コードレビューをしない選択 #でぃーぷらすトウキョウ
kajitack
3
1.3k
あなたはユーザーではない #PdENight
kajitack
4
390
生成AI時代の学び方 #第3木曜LT会
kajitack
0
110
例外処理とどう使い分ける?Result型を使ったエラー設計 #burikaigi
kajitack
17
6.6k
例外処理を理解して、設計段階からエラーを見つけやすく、起こりにくく #phpconfuk
kajitack
15
7.8k
フロントエンドのmonorepo化と責務分離のリアーキテクト
kajitack
2
320
Other Decks in Programming
See All in Programming
Strategy for Finding a Problem for OSS: With Real Examples
kibitan
0
130
Symfony + NelmioApiDocBundle を使った スキーマ駆動開発 / Schema Driven Development with NelmioApiDocBundle
okashoi
0
260
おれのAgentic Coding 2026/03
tsukasagr
1
130
生成 AI 時代のスナップショットテストってやつを見せてあげますよ(α版)
ojun9
0
340
L’IA au service des devs : Anatomie d'un assistant de Code Review
toham
0
190
野球解説AI Agentを開発してみた - 2026/02/27 LayerX社内LT会資料
shinyorke
PRO
0
390
GoのDB アクセスにおける 「型安全」と「柔軟性」の両立 - Bob という選択肢
tak848
0
300
Codex CLIのSubagentsによる並列API実装 / Parallel API Implementation with Codex CLI Subagents
takatty
2
810
AI-DLC 入門 〜AIコーディングの本質は「コード」ではなく「構造」〜 / Introduction to AI-DLC: The Essence of AI Coding Is Not “Code” but “Structure”
seike460
PRO
0
210
最初からAWS CDKで技術検証してもいいんじゃない?
akihisaikeda
4
180
Java 21/25 Virtual Threads 소개
debop
0
320
Everything Claude Code OSS詳細 — 5層構造の中身と導入方法
targe
0
160
Featured
See All Featured
The Illustrated Children's Guide to Kubernetes
chrisshort
51
52k
ピンチをチャンスに:未来をつくるプロダクトロードマップ #pmconf2020
aki_iinuma
128
55k
Dominate Local Search Results - an insider guide to GBP, reviews, and Local SEO
greggifford
PRO
0
130
Have SEOs Ruined the Internet? - User Awareness of SEO in 2025
akashhashmi
0
310
Unlocking the hidden potential of vector embeddings in international SEO
frankvandijk
0
230
Principles of Awesome APIs and How to Build Them.
keavy
128
17k
Responsive Adventures: Dirty Tricks From The Dark Corners of Front-End
smashingmag
254
22k
How to build a perfect <img>
jonoalderson
1
5.3k
[SF Ruby Conf 2025] Rails X
palkan
2
910
Balancing Empowerment & Direction
lara
5
1k
Crafting Experiences
bethany
1
110
Darren the Foodie - Storyboard
khoart
PRO
3
3.1k
Transcript
None
梶川 琢馬 𝕏 @kajitack 株式会社 TechBowl VPoT TechTrain の開発やメンターを担当してます! 関数型まつりコアスタッフ
コミュニティかわらばんに出展してます! X でスライド公開してます! https://x.com/kajitack 2/36
None
今日のゴール ① メソッド分割の限界を「結合度」で理解する ② ドメインイベントでリファクタリングする方法を知る ③ 同期→非同期の段階的な導入ステップを知る 4/36
ある処理を変更したら 関係ない処理まで動かなくなった 5/36
ユーザー登録のよくあるコード 適切にメソッドが分割されていて、見通しの良いコード? メール送信の仕様が変わるだけで、ユーザー登録全体のテストが壊れる 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
分割しても依存構造は変わらない 呼び出し側が全ての処理を知っている 7/36
なぜ分割しても 解決しないのか 8/36
結合度(Coupling) コンポーネント間の依存関係の強さ。結合度が高いと変更が連鎖する 疎結合にすれば、変更の影響をそのコンポーネント内に閉じ込められる 密結合(⾼結合) コンポーネント同⼠が互いを直接知っている 疎結合(低結合) コンポーネント同⼠が互いを知らない コンポーネントA ▲ 変更が発⽣
コンポーネントB コンポーネントC コンポーネントD A の変更 → B, C, D すべてに影響 修正・テストが連鎖的に必要になる コンポーネントA ▲ 変更が発⽣ コンポーネントB コンポーネントC インターフェース / イベント A の変更 → C, B には影響しない インターフェース経由で独⽴を保つ 9/36
結合度の2つの軸: 振る舞い × 時間 メソッド分割では右上のまま。ドメインイベントで左上→左下へ段階的に移動できる 振る舞い的結合(統合強度) 時間的結合 コントラクト結合 事実の通知 機能結合
振る舞いの指⽰ 低 ⾼ 機能結合 ̶ 呼び出し側が仕事を知っている 巨⼤メソッド メソッド分割 サービス分割 Step 0 分割しても 結合の種類は変わらない コントラクト結合 ̶ 事実だけを共有する 同期イベント 振る舞い的結合を解消 時間的結合は残る ⾮同期イベント 両⽅の結合を解消 Step 1 機能結合 → コントラクト結合 Step 2 同期 → ⾮同期 10/36
「振る舞い」を指示するか、「事実」を通知するか 「コマンド(これから実行すべき操作を表現したメッセージ)」 「イベント(すでに起こった変化を表現した事実)」 コマンド型 「メールを送れ」と指⽰ UserController sendEmail() notifyAdmin() createPlan() 送信側が受信側の仕事を知っている
振る舞い的結合が残る イベント型 「ユーザ���が作成された」と通知 UserController UserCreatedEvent MailSubscriber NotifySubscriber PlanSubscriber 送信側は受信側の存在を知らない 振る舞い的結合を断ち切れる 11/36
ドメインイベントとは 業務領域で発生した重要な出来事を表現するメッセージ 過去形で表現される取り消せない事実 「ユーザーが登録された」「注文が確定した」「在庫が切れた」 出来事を説明するために必要なデータを含む 12/36
ドメインイベント分析 ビジネス側の「〜したら」「〜のとき」という 言葉がヒント 1. 事業活動の出来事を過去形で洗い出す 2. 時系列に並べ、分岐や代替シナリオも整理する 3. イベントを引き起こす 「コマンド」や境界を発見する
13/36
Publisher と Subscriber Publisher は「何が起きたか」を発行するだけ Subscriber は関心のあるイベントだけを受け取る Publisher UserCreateUsecase イベントを発⾏する
publish Event UserCreatedEvent 「何が起きたか」の通知 Subscriber handle MailSubscriber NotifySubscriber PlanSubscriber Publisher → Subscriber の 直接のつながりはない Publisher と Subscriber はイベントだけを共有し、 互いの存在を知らない 14/36
依存の方向が逆転する Before ユーザー登録が 全ての副作用を呼び出す After 各処理が イベントに反応する 15/36
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
After: イベント発行の確認だけ 副作用が増えてもこのテストは変わらない #[Test] public function ユーザーを作成する() { $this->mock(DomainEventPublisher::class) ->expects($this->once())
->method('publish'); app()->make(UserCreateUsecase::class) ->exec(new UserId('1'), new Email('
[email protected]
')); } 17/36
振る舞い的結合は ドメインイベントで解きほぐす メソッド分割: 見通しは良くなるが、振る舞い的結合は残る ドメインイベント:「事実の通知」で振る舞い的結合を断ち切る → 依存の方向が逆転し、テストと変更がそれぞれ独立に 18/36
同期ドメインイベント → 非同期化の2ステップ 19/36
Step 1: ドメインイベントで 振る舞い的結合を解消 同期処理のまま、依存の方向を逆転させる 振る舞い的結合(統合強度) 時間的結合 コントラクト結合 事実の通知 機能結合
振る舞いの指⽰ 低 ⾼ 機能結合 ̶ 呼び出し側が仕事を知っている 巨⼤メソッド メソッド分割 サービス分割 Step 0 分割しても 結合の種類は変わらない コントラクト結合 ̶ 事実だけを共有する 同期イベント 振る舞い的結合を解消 時間的結合は残る ⾮同期イベント 両⽅の結合を解消 Step 1 機能結合 → コントラクト結合 Step 2 同期 → ⾮同期 20/36
Subscriber Publisher Repository Domain Model Usecase Subscriber Publisher Repository Domain
Model Usecase インスタンス作成 イベント作成 Publisher の登録 永続化処理 ドメインイベントを取得 イベントの発⾏ イベントを受け取る ドメインイベントの流れ 1. ドメインモデルがイベントを生成・保持する 2. 永続化した後にイベントを 取り出し、Publisher で発行する 3. Subscriber がイベントに反応して処理を実行する 同期/非同期の違いは Subscriber の実行タイミング 21/36
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
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
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
同期処理だけで テスト・変更・依存が改善する Before テスト: モック N 個 変更: 副作用追加で Usecase
を修正 依存: Usecase が全サービスに依存 After テスト: モック 1 個 変更: Subscriber を追加するだけ 依存: EventPublisher のみに依存 25/36
Step 2: Subscriber の実行タイミングを変える ユーザーはイベント発行後すぐにレスポンスを受け取れる → 時間的結合の解消 振る舞い的結合(統合強度) 時間的結合 コントラクト結合
事実の通知 機能結合 振る舞いの指⽰ 低 ⾼ 機能結合 ̶ 呼び出し側が仕事を知っている 巨⼤メソッド メソッド分割 サービス分割 Step 0 分割しても 結合の種類は変わらない コントラクト結合 ̶ 事実だけを共有する 同期イベント 振る舞い的結合を解消 時間的結合は残る ⾮同期イベント 両⽅の結合を解消 Step 1 機能結合 → コントラクト結合 Step 2 同期 → ⾮同期 26/36
非同期化の手段: メッセージングパターン 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
非同期化の問題: ロールバック時に イベントだけ残る DB コミットとイベント発行が別タイミングになる ロールバックしたのにイベントだけ発行されてしまう → データは無いのに Subscriber が動いてしまう
28/36
解決: イベントを DB に保存する (Transactional outbox パターン) DB トランザクション内にイベントも保存し、別プロセスで配信する ロールバックすればイベントも一緒に消える
→ 整合性を保証 29/36
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
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
適切な粒度でドメインイベントを 発行し、メッセージの順序を守る 顧客管理 UserCreated 販売促進 広告最適化 AdOptimized レポート出⼒ レポート出⼒が必要なのは UserCreated
ではなく AdOptimized 正しいドメインイベントに反応すれば順序依存は⽣まれない 32/36
段階的に進める 同期処理だけでも十分価値がある 非同期化は必要になってから Step 1: 同期処理 振る舞い的結合を解消 導入コストが低い すぐに始められる Step
2: 非同期化 時間的結合も解消 パフォーマンス改善 注意: 整合性→Outbox、重複→冪等性、順序→正しい イベントへの反応 33/36
ドメインイベントで ビジネスロジックを解きほぐす ビジネスロジックを「結合」の観点から見直し、 「ドメインイベント」で段階的に整理する ドメインイベント:「事実の通知」で依存の方向を逆転。各 Subscriber が独立して動き、追加は Subscriber を足すだけ。 変更も
Subscriber に閉じ、テストのモックも激減する。 ドメインイベントの非同期化: 重い処理の時間的な依存を 解消する。整合性・重複・順序の課題には Outbox・冪等性・ 順序非依存、補償トランザクションで対処。ドメインイベントを 同期的に処理するだけでも振る舞いの結合は解消されるので、 段階的に進める。 34/36
参考資料 「実践ドメイン駆動設計」Vaughn Vernon 著 / 髙木正弘訳 「ドメイン駆動設計をはじめよう」Vlad Khononov 著 /
増田亨監訳 / 黒田樹訳 「ソフトウェア設計の結合バランス」Vlad Khononov 著 / 島田浩二訳 35/36
None