# 注文の例から学ぶ DDD と ddd-toolkit
## 初めての方へ
**[getting_started.rs](getting_started.rs)** では、注文の例を通して、業務ルールを
コードで表し、保存・通知につなぐ流れを学びます。
Rust の構造体・enum・trait・`Result` の基礎が前提です。DDD、DB、非同期処理の
事前知識は不要です。リポジトリのルートで次を実行してください。
```sh
cargo run -p ddd-toolkit --example getting_started --locked
```
期待する標準出力(Cargoのビルドログは別です):
```text
getting_started: order #1 confirmed; saved total=4200 yen; notifications=1
```
### 何を作るのか
小さな店の「注文」を扱います。下書きの注文に1200円と3000円の商品を追加し、
確定した注文を保存し、確認メールを手配する担当へ「注文が確定された」と通知します。
実際のメールは送らず、通知をメモリに記録して結果を確認します。
守りたい約束は「価格は正」「空の注文は確定できない」「確定後は変更・再確定できない」。
`Order::add_item` と `Order::confirm` にこのルールを置きます。
呼び出し側ごとに検証すると、別の入口から変更したときにルールが抜けるためです。
`derive` で toolkit のトレイトを実装し、業務メソッドは開発者がルールに合わせて記述します。
### 用語とコードの対応
| ドメイン | ソフトウェアで扱う業務とそのルール | 注文の確定ルール |
| エンティティ | 内容が変わってもIDで同じものと識別する対象 | `Order`、`id` |
| 集約 / 集約ルート | 一緒に整合性を守る単位 / その変更の入口 | `Order` と非公開フィールド・業務メソッド |
| リポジトリー | 集約を保存・取得するための契約 | `Repository`、`MemoryOrders` |
| ドメインイベント | 業務で起きた事実。命令ではない | `OrderConfirmed` |
| アプリケーション層 | 業務メソッド、保存、通知の順序を調整する部分 | `main` |
| アダプター | 契約を具体的な保存先や通知先につなぐ実装 | `MemoryOrders`、`Notifications` |
DDD は業務の言葉・境界・ルールをモデルに反映する考え方です。
この例の集約は1つのエンティティで構成され、金額は整数の円で表します。
値オブジェクト(IDではなく値で区別する型)、複数集約、実システムの金額型、税、
商品明細、在庫、配送、決済はこの教材の範囲外です。
### コードの読む順番
1. **`OrderError` と `Order` のメソッド**: 業務の約束を確認します。
状態の変更を `add_item` と `confirm` に集め、`main` もこのメソッドを呼びます。
Rust の非公開フィールドはモジュール外からの直接アクセスを防ぎます。
この例の `main` は同じモジュールにあり、検証用にフィールドを参照しています。
2. **`main` の前半**: 空の注文の拒否 → 商品追加 → 確定。
`assert_eq!` は期待結果の確認であり、業務ルールの実装ではありません。
3. **保存 → 通知**: `save` は注文の状態を保存します。`record_event` は通知を保留するだけ。
`dispatch_events` を呼んで初めて `publish` が呼ばれ、成功すると保留キューが空になります。
状態の保存とイベント履歴の保存は別物です。この例はイベントソーシングではありません。
4. **末尾のアダプター**: 最後に戻って読めば十分です。`Infallible` はこのメモリ実装に
アダプター由来のエラーがないという型であり、実DBが失敗しないという意味ではありません。
### 自分で試す
- 商品価格を変え、合計金額のassertと出力も新しい期待値に合わせてください。
- 商品追加前に `confirm` を呼び、`EmptyOrder` が返ることを確かめてください。
- 確定後に `add_item` や `confirm` を呼び、`AlreadyConfirmed` が返り、合計と
イベント数が変わらないことを確かめてください。
この固定シナリオでは、成功を期待する操作に `unwrap` を使います。利用者入力や
外部I/Oのあるアプリケーションでは、エラーを呼び出し元へ返すなどの処理が必要です。
### 次に何を読むか
| 1 | [getting_started.rs](getting_started.rs) | 業務ルール、保存、通知 |
| 2 | [cqrs.rs](cqrs.rs) | 更新の命令と参照の質問を分ける。Specificationは真偽の条件、Policyは拒否理由付きの制約 |
| 3 | [dispatch_retry.rs](dispatch_retry.rs) | **発展: 配信の部分失敗と重複** |
| 任意 | [event_sourcing.rs](event_sourcing.rs) | **発展: 状態ではなく事実の履歴から復元**。revision、snapshot、upcasterを学ぶ |
| 任意 | [reliable_delivery.rs](reliable_delivery.rs) | **発展: asyncと障害対策**。outbox、projection、checkpoint、重複排除を学ぶ |
各ファイルは独立したプログラムで、題材に合わせたモデルを定義しています。
`cqrs` と `dispatch_retry` は、金額と保留イベントを持つ `Order` とアダプターを
[support/mod.rs](support/mod.rs) で共有します。
CQRS は更新と参照の責務を分ける設計です。この例では、コマンドとクエリが同じ
Repository を使います。ライフサイクルコールバックで保存・配信の順番を明示します。
保存と配信は別操作で、DBトランザクションとしての原子性はありません。
## 全例の実行と期待結果
リポジトリのルートで次を実行してください。全例が facade のみを使い、`derive` feature
(既定で有効)を必要とし、assert で期待結果を確認します。追加依存、DB、ブローカー、
非同期ランタイムは不要です。
```sh
cargo build -p ddd-toolkit --examples --all-features --locked
cargo run -p ddd-toolkit --example getting_started --features derive --locked
cargo run -p ddd-toolkit --example cqrs --features derive --locked
cargo run -p ddd-toolkit --example dispatch_retry --features derive --locked
cargo run -p ddd-toolkit --example event_sourcing --features derive --locked
cargo run -p ddd-toolkit --example reliable_delivery --features derive --locked
```
- [getting_started.rs](getting_started.rs): 注文の業務ルール → 確定 → 保存 → 通知。
合計4200円、通知1件。空注文・0円・確定後の変更を拒否します。
- [cqrs.rs](cqrs.rs): 加算ゼロを Specification、合計5000超過を Policy で拒否し、
明示的なライフサイクルコールバックで保存・配信します。クエリ結果は4200です。
- [dispatch_retry.rs](dispatch_retry.rs): 注文を保存し、最初のイベントを配信した後に
失敗させ、復元されたバッチを再試行します。配信結果は `[1, 1, 2]`。
保存は保留イベントを消費せず、配信失敗も保存をロールバックしません。
- [event_sourcing.rs](event_sourcing.rs): v1イベントをロード時にアップキャストして再生し、
スナップショットを保存した後、`ExpectedRevision` を指定して追記します。古いリビジョンでの
追記は状態を変えずに拒否します。スナップショット+後続イベントも全履歴再生も結果は5、
リビジョンは1(ゼロ始まり)です。アップキャストはアダプターのロード境界で行い、
再生は保留イベントを記録しません。スナップショット利用後も全履歴を保持し、圧縮はしません。
- [reliable_delivery.rs](reliable_delivery.rs): 非同期アウトボックスのライフサイクルで口座保存と
イベント投入を行います。ブローカー、チェックポイント保存、ack の失敗後も claim を再試行
できます。安定したメッセージIDで重複排除し、3回の配信成功でもプロジェクション更新は1回
(残高10)です。適用後、ack前にチェックポイントを保存します。保存失敗後の再適用は
`AlreadyApplied` ですが、チェックポイントは再保存します。実ブローカーの consumer を動かす
代わりにプロジェクションを直接呼びます。
## 発展例の用語
- **revision / ExpectedRevision**: ストリーム(1集約のイベント列)の位置 / 書き込み前に
期待する位置。同時更新で他の書き込みが先に進んでいれば拒否して上書きを防ぎます。
- **replay / snapshot / upcaster**: 過去イベントの適用による復元 / 途中状態のキャッシュ /
旧イベント形式を現在の形式へ変換する処理。例ではCounterの加算履歴を扱います。
- **outbox**: 状態保存と同じトランザクションで、後で送る通知を記録する置き場。
- **projection**: イベントから作る参照用の状態。例では入金イベントから残高を作ります。
- **checkpoint**: 読み手の処理位置の記録。位置だけでは二重適用を防げないため、
安定したIDを使う重複排除も必要です。
- **at-least-once**: 同じ通知が複数回届き得る配信。入金を二度加算しないよう、
同じイベントを繰り返しても効果が増えない(冪等な)適用を行います。
イベント通知は業務上の事実を他の処理へ伝え、イベントソーシングはその履歴から状態を
復元します。アウトボックスは状態保存と通知の間で障害が起きる場合に備える仕組みです。
それぞれの役割を踏まえ、アプリケーションに必要なものを選んでください。
## アダプターの責任範囲
すべての保存状態・重複排除状態は**インメモリ**で、プロセス終了時に失われます。
小さなアプリケーション側の実装例であり、toolkit 組み込みのアダプターや本番の永続性保証
ではありません。本番では以下が必要です。
- 集約状態とアウトボックスへの投入を、アダプターのトランザクションで原子的に永続化します。
楽観ロックのリビジョン確認と追記も原子的に行います。ライフサイクルコンテキストだけでは
DBトランザクション、ロールバック、クラッシュ復旧を提供しません。
- claim のリース、複数ワーカーの所有権、復旧、再試行・バックオフを定めます。
この例は口座1つ・ワーカー1つで、ackまでキュー先頭を再claimします。
- at-least-once 配信を前提とします。配信成功後のack失敗で重複配信が起こります。
プロジェクション更新と重複排除マーカーを原子的に永続化してください。
別操作のチェックポイント保存が失敗しても再生で効果を二重適用しない設計が必要です。
- 永続カーソルの意味・順序を定めます。この例のカーソルは安定したメッセージIDであり、
ソート可能なストリーム位置ではなく、大小比較もしません。
- 非同期契約はランタイムを選ばず、ボックス化した `Send` Future を返します。
例の `ready` は1回だけpollして `Pending` を拒否するもので、汎用executorではありません。
実I/Oにはアプリケーションでexecutorを選び、アダプターでキャンセル・エラー復旧を設計します。
アウトボックスのライフサイクルは失敗・キャンセル時にもイベントバッチを消費します。
コマンドや投入を無条件に繰り返すと効果の欠落・重複が起こり得るため、コミット結果が不確実な
場合は、このヘルパーではなく永続的なアプリケーション/アダプターの復旧で解決してください。
CIは通常のワークスペーステストとfeature-matrixに加え、5例を明示的にビルド・実行します。
`required-features = ["derive"]` により、no-default-features の検査ではderive依存の例を除外します。