Movatterモバイル変換


[0]ホーム

URL:


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

イベントストーミング図からコードへの変換手順 / Procedure for Converti...

Avatar for nrs nrs
June 28, 2025

イベントストーミング図からコードへの変換手順 / Procedure for Converting Event Storming Diagrams to Code

PHP Conference 2025 Japan における発表資料です。
イベントストーミング図をコードへ落とし込むやり方をお話しています。

https://fortee.jp/phpcon-2025/proposal/7368450d-070c-4d23-a12f-37371d5c7947

# URL
YouTube:https://www.youtube.com/c/narusemi
HomePage:https://nrslib.com
Twitter:https://twitter.com/nrslib
Instagram:https://www.instagram.com/nrslib/

Avatar for nrs

nrs

June 28, 2025
Tweet

More Decks by nrs

See All by nrs

Other Decks in Programming

See All in Programming

Featured

See All Featured

Transcript

  1. イベントストーミング図からコードへの変換手順 nrs

  2. 2 Profile nrs(成瀬 允宣) @nrslib コドモンのCTO 趣味: カンファレンス講演 学生支援 小学校支援

    写真
  3. 3 コドモンからは…… 7名!?

  4. 4 プロポーザル

  5. 5 プロポーザル

  6. 6 プロポーザル

  7. 7 プロポーザル

  8. イベントストーミング基礎 具体的な実装への落とし込み まとめ イベントストーミング図からコードへの変換手順

  9. イベントストーミング基礎 具体的な実装への落とし込み まとめ イベントストーミング図からコードへの変換手順

  10. モデリングって結局どれがいいのさ?

  11. 11 • モデリングの常套手段は? ドメイン駆動設計 色々あってみんないい

  12. 私は勝ち馬に乗りたいです

  13. 13 • イベントストーミングに全部賭けろ モデリングの常套手段として イベントを起点に ビジネスプロセスや システムの モデリングができる ワークキングセッション

  14. 14 • アクター 構成物の説明 コマンド(後述)を 実行する主体

  15. 15 • コマンド 構成物の説明 何らかのアクションを 実行する意図や要求

  16. 16 • 集約 構成物の説明 コマンドを処理する オブジェクト

  17. 17 • イベント 構成物の説明 ビジネス上で発生した 重要な出来事や事実

  18. 18 • リードモデル 構成物の説明 データの読み取りに 最適化された データ構造やビュー

  19. 19 • ポリシー 構成物の説明 自動化された 判断ロジック 条件をいれるのは nrs流

  20. 20 • ポリシー 構成物の説明 空欄のポリシーは 「必ず」の意味 ※ポリシーに名前をつけても あまり役に立たないので 空欄にするのは nrs

  21. 21 • 外部システム 構成物の説明 対象システムの 境界外にあるシステム

  22. 22 • 複数のイベント 構成物の説明 結果の分岐や 複数の出来事が並列に起きる ことを表す

  23. 23 • 付箋は特定の付箋からしか繋げられない ルール 左図の矢印の 繋がり方以外は NG

  24. 24 • パターン1 ルール リードモデルから イベントまでの パターン

  25. 25 • パターン2 ルール ポリシーが 中継するパターン

  26. 26 • 割合どこからでも始められる 起点 よくある起点は アクター イベント(特に外部絡み) リードモデル

  27. 27 • 大きく2つ ◦ ワーキングセッション ▪ メンバーが全員やり方や主旨を理解した上で取り組む ▪ Pros:一気にすべてが進む ▪

    Cons:初めてやると上手く進まない、長時間かかる ◦ ヒアリング ▪ ファシリテーターがヒアリングをして進める ▪ Pros:初めてやってもまとまりやすい、短時間でやれる ▪ Cons:ファシリテーターの技量に依存する部分がある イベントストーミングのやり方
  28. イベントストーミング見てぇよなぁ!

  29. 29 • @nrslib をフォローして情報をお待ち下さい(被験体も募集) イベントをやりましょう

  30. イベントストーミング基礎 具体的な実装への落とし込み まとめ イベントストーミング図からコードへの変換手順

  31. 31 • 今回のセッションはバイブコーディングにすべてを賭けました 流行りに乗るぜ!

  32. 32 • バリエーション豊かにご用意しました ◦ トランザクションスクリプト ◦ ドメインオブジェクト ◦ 複合パターン ◦

    バッチ • あえて用意してません ◦ CQRS+ESパターン コードの種類
  33. 33 • まずは単純なもの 処理を見ていく アクターから コマンドが投げられ リードモデルに繋ぐ

  34. class DocumentController extends Controller { public function store(Request $request): JsonResponse

    { $request->validate([ 'contents' => 'required|string', ]); $command = new CreateDocumentCommand($request->input('contents')); $document = $this->documentApplicationService->createDocument($command); return response()->json([ 'id' => $document->id, 'contents' => $document->contents, 'effective_count' => $document->effectiveCount(), 'created_at' => $document->created_at, ], 201); } }
  35. class DocumentApplicationService { public function createDocument(CreateDocumentCommand $command): Document { return

    Document::create([ 'contents' => $command->contents, ]); } }
  36. 36 次の処理の確認

  37. class DocumentController extends Controller { public function update(Request $request, int

    $id): JsonResponse { $request->validate([ 'contents' => 'required|string', ]); $command = new UpdateDocumentCommand($id, $request->input('contents')); $document = $this->documentApplicationService->updateDocument($command); return response()->json([ 'id' => $document->id, 'contents' => $document->contents, 'effective_count' => $document->effectiveCount(), 'updated_at' => $document->updated_at, ]); } }
  38. class DocumentApplicationService { ... public function updateDocument(UpdateDocumentCommand $command): Document {

    $document = Document::findOrFail($command->id); $document->update([ 'contents' => $command->contents, ]); return $document->refresh(); } }
  39. 39 次の処理の確認

  40. public function markEffective(Request $request, int $id): JsonResponse { $request->validate([ 'user_id'

    => 'required|integer|exists:users,id', ]); $command = new MarkDocumentEffectiveCommand($id, $request->input('user_id')); $document = $this->effectiveApplicationService->markDocumentEffective($command); return response()->json([ 'id' => $document->id, 'contents' => $document->contents, 'effective_count' => $document->effectiveCount(), 'updated_at' => $document->updated_at, ]); }
  41. public function markDocumentEffective(MarkDocumentEffectiveCommand $command): Document { $document = Document::findOrFail($command->documentId); $user

    = User::findOrFail($command->userId); Effective::firstOrCreate([ 'document_id' => $document->id, 'user_id' => $user->id, ]); return $document->refresh(); }
  42. 42 次の処理の確認

  43. public function unmarkEffective(Request $request, int $id): JsonResponse { $request->validate([ 'user_id'

    => 'required|integer|exists:users,id', ]); $command = new UnmarkDocumentEffectiveCommand($id, $request->input('user_id')); $document = $this->effectiveApplicationService->unmarkDocumentEffective($command); return response()->json([ 'id' => $document->id, 'contents' => $document->contents, 'effective_count' => $document->effectiveCount(), 'updated_at' => $document->updated_at, ]); }
  44. public function unmarkDocumentEffective(UnmarkDocumentEffectiveCommand $command): Document { $document = Document::findOrFail($command->documentId); $user

    = User::findOrFail($command->userId); Effective::where([ 'document_id' => $document->id, 'user_id' => $user->id, ])->delete(); return $document->refresh(); }
  45. 45 • ポリシーが絡む処理 ちょっと変化球

  46. class AlbumController extends Controller { public function store(Request $request): JsonResponse

    { $request->validate([...]); try { $command = new CreateAlbumCommand( name: $request->input('name'), description: $request->input('description'), userId: $request->input('user_id'), isPublic: filter_var($request->input('is_public', true), FILTER_VALIDATE_BOOL photo: $request->file('photo') ); $album = $this->albumApplicationService->createAlbum($command); // アルバムの写真数を含めてレスポンス return response()->json([ 'id' => $album->getId()?->getValue(), 'name' => $album->getName(), 'description' => $album->getDescription(), 'user_id' => $album->getUserId()->getValue(), 'is_public' => $album->isPublic(),
  47. class AlbumApplicationService { ... public function createAlbum(CreateAlbumCommand $command): Album {

    $album = Album::create( name: $command->name, description: $command->description, userId: new UserId($command->userId), isPublic: $command->isPublic ); // リポジトリを使用してアルバムを保存 $savedAlbum = $this->albumRepository->save($album); // ポリシー: 写真が同時にアップロードされた場合の条件分岐 if ($command->photo !== null) { // アルバムに写真を追加 $this->addPhotoToAlbum($savedAlbum, $command->photo); } return $savedAlbum; } }
  48. class AlbumApplicationService { ... public function addPhotoToAlbum(Album $album, UploadedFile $file):

    Photo { $filename = uniqid() . '.' . $file->getClientOriginalExtension(); $path = $file->storeAs('photos', $filename, 'public'); // ドメインエンティティとしてPhotoを作成 $photo = Photo::create( filename: $filename, originalName: $file->getClientOriginalName(), mimeType: $file->getMimeType(), fileSize: $file->getSize(), path: $path ); $savedPhoto = $this->photoRepository->save($photo); $album->addPhoto($savedPhoto); $this->albumRepository->save($album); return $savedPhoto; }
  49. interface AlbumRepositoryInterface { public function save(Album $album): Album; public function

    findById(AlbumId $id): ?Album; public function findByUserId(UserId $userId): array; public function delete(AlbumId $id): void; }
  50. class AlbumApplicationService { ... public function addPhotoToAlbum(Album $album, UploadedFile $file):

    Photo { $filename = uniqid() . '.' . $file->getClientOriginalExtension(); $path = $file->storeAs('photos', $filename, 'public'); // ドメインエンティティとしてPhotoを作成 $photo = Photo::create( filename: $filename, originalName: $file->getClientOriginalName(), mimeType: $file->getMimeType(), fileSize: $file->getSize(), path: $path ); $savedPhoto = $this->photoRepository->save($photo); $album->addPhoto($savedPhoto); $this->albumRepository->save($album); return $savedPhoto; } }
  51. class Album { public function __construct( private ?AlbumId $id, private

    string $name, private ?string $description, private UserId $userId, private bool $isPublic, private array $photoIds = [], private ?\DateTimeImmutable $createdAt = null ) { } ... public function addPhoto(Photo $photo): void { $this->photoIds[] = $photo->getId(); } }
  52. 52 次の処理の確認

  53. class AlbumController extends Controller { ... public function addPhoto(Request $request,

    int $albumId): JsonResponse { $request->validate([ 'photo' => 'required|image|max:2048', ]); try { $command = new AddPhotoToAlbumCommand( albumId: $albumId, photo: $request->file('photo') ); $photo = $this->albumApplicationService->addPhotoToAlbumById( $command->albumId, $command->photo ); return response()->json([...], 201); } catch (Exception $e) { return response()->json([
  54. class AlbumApplicationService { ... public function addPhotoToAlbumById(int $albumId, UploadedFile $file):

    Photo { $album = $this->albumRepository->findById(new AlbumId($albumId)); if ($album === null) { throw new \Exception('Album not found'); } return $this->addPhotoToAlbum($album, $file); } }
  55. 55 • 外部システムとイベントによる分岐 さらなる変化球

  56. class OrderController extends Controller { public function placeOrder(Request $request): JsonResponse

    { $validated = $request->validate([...]); $command = new CreateOrderCommand( (int) $validated['user_id'], (float) $validated['amount'], $validated['currency'] ?? 'JPY' ); $order = $this->orderApplicationService->placeOrder($command); return response()->json([ 'id' => $order->id()->value(), 'user_id' => $order->userId()->getValue(), 'amount' => $order->amount(), 'currency' => $order->currency(), 'status' => $order->status()->value, 'payment_completed' => $order->isPaymentCompleted(), 'transaction_id' => $order->paymentTransactionId(), 'created_at' => $order->createdAt()->format('c'), ], 201);
  57. class OrderApplicationService { public function placeOrder(CreateOrderCommand $command): Order { $orderId

    = new OrderId(Str::uuid()->toString()); $userId = new UserId($command->userId); $order = Order::create($orderId, $userId, $command->amount, $command->currency); $paymentResult = $this->paymentGateway->processPayment($order); if ($paymentResult->isSuccess()) { $order->completePayment($paymentResult->transactionId()); $cart = $this->cartRepository->findByUserId($userId); if ($cart) { $cart->clear(); $this->cartRepository->save($cart); } } else { $order->failPayment($paymentResult->transactionId()); } $this->orderRepository->save($order); return $order;
  58. 58 • 外部システムとイベントによる分岐 バッチ

  59. public function processPrintOrders(): array { // 1. 購入履歴から印刷対象の注文を取得 $completedOrders =

    $this->orderRepository- >findCompletedOrdersWithoutPrintOrder(); $results = []; // 2. 100件ずつのバルク処理 $chunks = array_chunk($completedOrders, self::BATCH_SIZE); foreach ($chunks as $orderBatch) { $batchResults = $this->processBatch($orderBatch); $results = array_merge($results, $batchResults); } return array_map(fn(PrintOrderResult $result) => $result->toArray(), $results); }
  60. private function processBatch(array $orders): array { $printOrders = []; $orderMap

    = []; $results = []; // 1. 印刷依頼をまとめて作成 foreach ($orders as $order) { try { $printOrder = PrintOrder::create( $order->id(), $order->photoIds() ); $printOrders[] = $printOrder; $orderMap[$printOrder->id()->value()] = $order; } catch (\Exception $e) { $results[] = PrintOrderResult::failure( $order->id()->value(), $e->getMessage() ); } } if (empty($printOrders)) {
  61. if (empty($printOrders)) { return $results; } try { // 2.

    100件まとめて印刷会社に送信 $batchResults = $this->printingService->sendBatchToPrinter($printOrders); // 3. 成功した印刷依頼の状態を更新 $successfulPrintOrders = []; foreach ($printOrders as $printOrder) { $printOrderId = $printOrder->id()->value(); $order = $orderMap[$printOrderId]; $success = $batchResults[$printOrderId] ?? false; if ($success) { $printOrder->sendToPrinter(); $successfulPrintOrders[] = $printOrder; $results[] = PrintOrderResult::success( $order->id()->value(), $printOrder->id()->value() ); } else { $results[] = PrintOrderResult::failure(
  62. $results[] = PrintOrderResult::failure( $order->id()->value(), 'Failed to send to printer' );

    } } // 4. 成功した印刷依頼をバルクで保存 if (!empty($successfulPrintOrders)) { $this->printOrderRepository->saveBatch($successfulPrintOrders); } } catch (\Exception $e) { // バッチ処理全体が失敗した場合 foreach ($printOrders as $printOrder) { $order = $orderMap[$printOrder->id()->value()]; $results[] = PrintOrderResult::failure( $order->id()->value(), 'Batch processing failed: ' . $e->getMessage() ); } } return $results;
  63. イベントストーミング基礎 具体的な実装への落とし込み まとめ イベントストーミング図からコードへの変換手順

  64. 64 • 今日使ってた図はビジネスプロセスモデリング システムモデリング 従来的なプログラム設計でもある程度役に立つ 図ですべてを表現することは目的ではなく 役立つドキュメントとして活用するのがおすすめ コードと図を一致させるならイベントソーシングで

  65. 65 • 今日の図はビジネスプロセスモデリング 実装に役立つ図について

  66. 66 • システムモデリング 実装に役立つ図について より実装に即して まとめるフェーズが ある

  67. 67 • イベントストーミングとの付き合い方 まとめ 従来的なプログラム設計でもある程度役に立つ 図ですべてを表現することは目指すのではなく 役立つドキュメントとして活用するのがおすすめ もしコードと図を一致させたいならイベントソーシングで

  68. 68 • X ◦ @nrslib • HomePage ◦ https://nrslib.com/ •

    YouTube ◦ https://www.youtube.com/c/narusemi おしまい イベントストーミングしたかったらコドモンにおいでよ↓

[8]ページ先頭

©2009-2025 Movatter.jp