背景
Claudeに練習問題と模範解答、自分の解答とそれに対するフィードバックを受けることで、以前に学んだ知見を実務で活かすための基礎固めをしたいと思います。
https://qiita.com/kumab2221/items/ea608da157508256001a
所感
練習問題を解いて見て、全くアウトプットが作れないということはないのだけれど、フィードバックを受けて、充分ではないと感じました。
今のレベルだと低レベルでは理解できているけど、実践には至っていないのかな?という印象です。
練習を通して(いつまで続くかわからないけど)、スキルアップしたいと考えています。
AggregateとEntityの使い分けに関する理解が怪しいですね...。
問題
要件 : あなたはオンライン書店のシステムを設計しています。
- 顧客はアカウントを作成し、メールアドレスとパスワードでログインする。
- 顧客は書籍を検索し、カートに追加できる。カート内の書籍の数量は変更可能。
- 書籍には ISBN、タイトル、著者、価格、在庫数がある。
- 顧客はカートの内容を確認し、注文を確定する。
- 注文には配送先住所が必要。配送先住所は「氏名・郵便番号・都道府県・市区町村・番地」で構成される。
- 注文確定時に在庫が不足している場合、注文は失敗する。
- 注文が確定すると、注文ステータスは「確定済み」→「発送済み」→「配達完了」と遷移する。
模範解答
エンティティ
| エンティティ | 識別子 | 主な属性・関連 |
|---|---|---|
| 顧客 (Customer) | customerId | メールアドレス, パスワード(ハッシュ) |
| 書籍 (Book) | ISBN | タイトル, 著者, 価格, 在庫数 |
| カート (Cart) | cartId (または customerId と 1:1) | カート明細のリスト |
| 注文 (Order) | orderId | 顧客, 注文明細のリスト, 配送先住所, 注文ステータス |
値オブジェクト
| 値オブジェクト | 構成要素 | 理由 |
|---|---|---|
| メールアドレス (Email) | 文字列(形式バリデーション付き) | 同じ文字列なら同一とみなせる |
| 配送先住所 (ShippingAddress) | 氏名, 郵便番号, 都道府県, 市区町村, 番地 | 属性の組み合わせで等価 |
| 金額 (Money) | 数値, 通貨 | 同額なら区別不要 |
| カート明細 (CartItem) | 書籍(への参照), 数量 | カート内の一行。個別IDは不要 |
| 注文明細 (OrderLine) | 書籍(への参照), 数量, 単価 | 注文確定時の価格スナップショット |
| 注文ステータス (OrderStatus) | 確定済み / 発送済み / 配達完了 | 列挙型で十分 |
集約
| 集約ルート | 含まれるもの | 整合性のルール |
|---|---|---|
| Book | Book 単体 | 在庫数は 0 以上 |
| Cart | Cart + CartItem | カート明細の数量は 1 以上 |
| Order | Order + OrderLine | 注文ステータスの遷移は一方向のみ |
| Customer | Customer 単体 | メールアドレスは一意 |
ドメインイベント
-
CartItemAdded— 商品がカートに追加された -
OrderPlaced— 注文が確定された(→ 在庫引き当てのトリガー) -
OrderShipped— 注文が発送された -
OrderDelivered— 注文が配達完了した
自分の解答
エンティティ
アカウント、注文、ログイン、配達先
値オブジェクト
メールアドレス、パスワード、タイトル、著者、価格、在庫数、注文ステータス、氏名、郵便番号、住所
集約
顧客、書籍、カート
ドメインイベント
注文する、ログインする
フィードバック・改善点
改善点 1: 「ログイン」「配達先」をエンティティにしている
エンティティかどうかの判断基準は 「それ自体に一意のIDを振って、ライフサイクルを追跡する必要があるか?」。
- ログイン → これは「行為」であり、モノではない。エンティティにはならない。認証はドメインモデルの外(アプリケーション層やインフラ層)の関心事。
- 配達先 → 「東京都新宿区○○」という住所は、同じ内容なら区別する必要がない。つまり値オブジェクト(ShippingAddress)が適切。
判断に迷ったら 「これにIDを振って、データベースで個別に追跡する意味があるか?」 と自問する。
改善点 2: 「アカウント」と「顧客」の混同
アカウントをエンティティにしているが、この要件では「アカウント」と「顧客」は実質的に同じもの。模範解答では Customer(顧客) として1つのエンティティにまとめている。要件の言葉に引きずられて同じ概念を分割しないよう注意。
改善点 3: タイトル・著者・価格・在庫数を値オブジェクトにしている
自己分析で「詳細すぎる」と気づいている通り。これらは Book エンティティの属性(メンバ変数)。値オブジェクトにすべきかどうかの基準は以下。
値オブジェクトにする → 複数の属性をまとめてバリデーションや振る舞いを持たせたい
例: 配送先住所(5つの属性をセットで扱い、フォーマット検証する)
例: Money(数値 + 通貨をセットで、計算ロジックを持つ)
単なる属性にする → プリミティブ型で十分で、特別なルールがない
例: タイトル → ただの String
例: 著者 → ただの String
「価格」は単なる数値ではなく Money(数値, 通貨) にすると通貨計算の振る舞いを持てるので値オブジェクトにする価値がある、という判断。
改善点 4: Entity と Aggregate の関係
Aggregate(集約)= 整合性の境界
└── Aggregate Root(集約ルート)= そのグループの代表エンティティ
└── 内部のエンティティ or 値オブジェクト
例: Order 集約
├── Order(集約ルート / エンティティ) ← orderId で識別
├── OrderLine(値オブジェクト) ← 注文の中の一行
└── ShippingAddress(値オブジェクト) ← 配送先
つまり 集約はエンティティを「含む」もの。集約ルート自体がエンティティであり、「外からアクセスする窓口」になる。この問題では Book・Cart・Order・Customer の4つが集約ルート兼エンティティ。
改善点 5: ドメインイベントの考え方
「注文する」「ログインする」と動詞で書いているが、ドメインイベントは 「起きた事実」を過去形 で表現する。
× 注文する → ○ OrderPlaced(注文が確定された)
× ログインする → ログインはドメインイベントではない(インフラの関心事)
「ビジネス上、他の処理のトリガーになる出来事は何か?」で考えると見つけやすい。OrderPlaced が発行されたら在庫を引き当てる、OrderShipped が発行されたら配送追跡を開始する、といった連鎖。
次の問題に向けたチェック手順
① 要件の名詞を全部リストアップ
② 各名詞に「IDで追跡する必要があるか?」を問う
Yes → エンティティ候補
No → 値オブジェクト候補 or ただの属性
③ 値オブジェクトは「複数属性のまとまり」or「バリデーション付きの値」か?
Yes → 値オブジェクト
No → エンティティの単なる属性
④ エンティティ群を「一緒に変更されるもの」でグルーピング → 集約
⑤ 要件の動詞から「ビジネス上重要な出来事」を過去形で → ドメインイベント
---
本記事の執筆にはClaude (Anthropic)を補助ツールとして使用し、
内容はすべて筆者が検証・確認しています。