Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

4 プロポーザル

Slide 5

Slide 5 text

5 プロポーザル

Slide 6

Slide 6 text

6 プロポーザル

Slide 7

Slide 7 text

7 プロポーザル

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

27 ● 大きく2つ ○ ワーキングセッション ■ メンバーが全員やり方や主旨を理解した上で取り組む ■ Pros:一気にすべてが進む ■ Cons:初めてやると上手く進まない、長時間かかる ○ ヒアリング ■ ファシリテーターがヒアリングをして進める ■ Pros:初めてやってもまとまりやすい、短時間でやれる ■ Cons:ファシリテーターの技量に依存する部分がある イベントストーミングのやり方

Slide 28

Slide 28 text

イベントストーミング見てぇよなぁ!

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

32 ● バリエーション豊かにご用意しました ○ トランザクションスクリプト ○ ドメインオブジェクト ○ 複合パターン ○ バッチ ● あえて用意してません ○ CQRS+ESパターン コードの種類

Slide 33

Slide 33 text

33 ● まずは単純なもの 処理を見ていく アクターから コマンドが投げられ リードモデルに繋ぐ

Slide 34

Slide 34 text

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); } }

Slide 35

Slide 35 text

class DocumentApplicationService { public function createDocument(CreateDocumentCommand $command): Document { return Document::create([ 'contents' => $command->contents, ]); } }

Slide 36

Slide 36 text

36 次の処理の確認

Slide 37

Slide 37 text

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, ]); } }

Slide 38

Slide 38 text

class DocumentApplicationService { ... public function updateDocument(UpdateDocumentCommand $command): Document { $document = Document::findOrFail($command->id); $document->update([ 'contents' => $command->contents, ]); return $document->refresh(); } }

Slide 39

Slide 39 text

39 次の処理の確認

Slide 40

Slide 40 text

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, ]); }

Slide 41

Slide 41 text

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(); }

Slide 42

Slide 42 text

42 次の処理の確認

Slide 43

Slide 43 text

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, ]); }

Slide 44

Slide 44 text

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(); }

Slide 45

Slide 45 text

45 ● ポリシーが絡む処理 ちょっと変化球

Slide 46

Slide 46 text

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(),

Slide 47

Slide 47 text

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; } }

Slide 48

Slide 48 text

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; }

Slide 49

Slide 49 text

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; }

Slide 50

Slide 50 text

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; } }

Slide 51

Slide 51 text

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(); } }

Slide 52

Slide 52 text

52 次の処理の確認

Slide 53

Slide 53 text

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([

Slide 54

Slide 54 text

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); } }

Slide 55

Slide 55 text

55 ● 外部システムとイベントによる分岐 さらなる変化球

Slide 56

Slide 56 text

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);

Slide 57

Slide 57 text

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;

Slide 58

Slide 58 text

58 ● 外部システムとイベントによる分岐 バッチ

Slide 59

Slide 59 text

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); }

Slide 60

Slide 60 text

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)) {

Slide 61

Slide 61 text

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(

Slide 62

Slide 62 text

$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;

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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