要件
オンライン学習プラットフォームを設計してください。
- 講師はコースを作成できる。コースにはタイトル、説明、カテゴリ、価格がある。
- コースは複数のセクションで構成され、各セクションには複数のレッスンがある。
- レッスンには「動画レッスン」と「テストレッスン」の 2 種類がある。動画レッスンには動画URL と再生時間がある。テストレッスンには問題リスト(複数選択式)がある。
- コースは「下書き」→「レビュー中」→「公開」のステータスを持つ。レビューは運営が行い、品質基準を満たさない場合は差し戻される。
- 受講者はコースを購入して受講を開始する。購入時にコースの価格がスナップショットとして記録される。
- 受講者はレッスンを順番に進め、各レッスンの完了状態が記録される。テストレッスンは正答率 80% 以上で合格。
- 全レッスンを完了すると修了証が発行される。修了証には受講者名、コース名、修了日が記載される。
- 受講者はコースにレビュー(1〜5の星評価+コメント)を投稿できる。レビューは1コースにつき1件まで。
- 講師には売上の 70% が報酬として支払われる。報酬は月次で集計される。
所感
指摘された内容について可能な限り意識して取り組んで入るが、まだまだと感じるなと思ってます
今回気になったのはEnrollmentが抜けているよという話だったんだけど、この点は受講生の集約にまとめたつもりでした
ただ、「一緒に変更されるもの」ではないので、その点の境界の分割が弱いという自己評価になりました
模範解答
エンティティ
| エンティティ | 識別子 | 主な属性・関連 |
|---|---|---|
| 講師 (Instructor) | instructorId | 氏名, プロフィール |
| コース (Course) | courseId | タイトル, 説明, カテゴリ, 価格, ステータス, 講師 |
| セクション (Section) | sectionId | タイトル, 表示順序 |
| レッスン (Lesson) | lessonId | タイトル, 表示順序, レッスン種別 |
| 受講者 (Student) | studentId | 氏名, メールアドレス |
| 受講登録 (Enrollment) | enrollmentId | 受講者, コース, 購入価格, 進捗, 修了状態 |
| レビュー (Review) | reviewId | 受講者, コース, 星評価, コメント |
| 報酬明細 (PayoutRecord) | payoutId | 講師, 対象年月, 金額, 支払ステータス |
値オブジェクト
| 値オブジェクト | 構成要素 | 理由 |
|---|---|---|
| コースステータス (CourseStatus) | 下書き/レビュー中/公開/差し戻し | 列挙型 |
| 動画情報 (VideoContent) | 動画URL, 再生時間 | 動画レッスンの内容詳細 |
| テスト内容 (QuizContent) | 問題リスト(問題文, 選択肢, 正解) | テストレッスンの内容詳細 |
| レッスン進捗 (LessonProgress) | レッスンへの参照, 完了フラグ, テストスコア | 受講登録内の一行 |
| 修了証 (Certificate) | 受講者名, コース名, 修了日 | 発行後は不変。独自ライフサイクル不要 |
| 金額 (Money) | 数値, 通貨 | 汎用値オブジェクト |
| 星評価 (Rating) | 1〜5 の整数 | バリデーション付きの値 |
| 合格基準 (PassingCriteria) | 正答率の閾値(80%) | ポリシーを明示化 |
集約と境界の理由
| 集約ルート | 含まれるもの | 境界の理由 |
|---|---|---|
| Course | Course + Section + Lesson (+ VideoContent / QuizContent) | コースの構成(セクション・レッスン)は一貫性を持って変更される必要がある。セクションやレッスンが単独で存在する意味はない。 |
| Enrollment | Enrollment + LessonProgress のリスト | 進捗管理は受講者×コースの単位で整合性が必要。「全レッスン完了 → 修了」の判定はこの集約内で行う。 |
| Review | Review 単体 | 「1コース1レビュー」の制約はあるが、コースや受講登録とは独立してライフサイクルを持つ。 |
| Instructor | Instructor 単体 | 講師プロフィールは独立管理。 |
| Student | Student 単体 | 受講者情報は独立管理。 |
| PayoutRecord | PayoutRecord 単体 | 報酬は月次集計の結果であり、他の集約とトランザクションを共有しない。 |
ドメインイベント
-
CourseCreated— コースが作成された -
CourseSubmittedForReview— コースがレビューに提出された -
CoursePublished— コースが公開された -
CourseRejected— コースが差し戻された -
CoursePurchased— コースが購入された(→ Enrollment 作成のトリガー) -
LessonCompleted— レッスンが完了した -
QuizPassed/QuizFailed— テストの合否 -
CourseCompleted— 全レッスン完了(→ 修了証発行のトリガー) -
CertificateIssued— 修了証が発行された -
ReviewPosted— レビューが投稿された
ドメインサービス
| サービス | 責務 |
|---|---|
| テスト採点サービス (QuizGradingService) | 受講者の回答を受け取り、正答率を計算し、合格基準と照合する。Lesson にも Enrollment にも属しにくい横断ロジック。 |
| 報酬集計サービス (PayoutCalculationService) | 月次で売上を集計し、70% の報酬額を算出して PayoutRecord を生成する。複数の集約(Enrollment の購入価格)を参照する。 |
| 修了証発行サービス (CertificateIssuanceService) | CourseCompleted イベントを受けて修了証を生成する。受講者名・コース名の取得が集約をまたぐため、サービスとして切り出す。 |
解説ポイント
- Course 集約を大きくしすぎない判断: Section と Lesson は Course に従属するが、受講者数が多い場合に Course 集約が肥大化する。Enrollment(受講登録)を別集約にすることで、コース定義と受講進捗の更新が競合しない。
- 修了証をエンティティではなく値オブジェクトにする理由: 修了証は一度発行されたら変更されない。再発行が必要なら新規作成する。独自の ID で管理する必要がなければ値オブジェクトで十分(※ 要件次第ではエンティティもあり得る)。
- Review の「1コース1件」制約: この制約は Review 集約単体では保証できない。ドメインサービスまたはリポジトリレベルで一意性を担保する設計が必要。
自分の解答
ユビキタス言語
オンライン学習プラットフォーム
コース情報(タイトル,説明,カテゴリ,価格)
コース状態(下書き,レビュー中,公開)
動画レッスン(URL,再生時間)
テストレッスン(問題リスト)
講師, 運営, 品質基準, 受講者
購入する, レッスンの完了状態(テストでは80%)
修了証(コース名、終了日)
コースレビュー
売上(70%, 月次で集計)
値オブジェクト
| 値オブジェクト | 構成要素 | 理由 |
|---|---|---|
| コース詳細 | タイトル,説明,カテゴリ,価格 | 最低限の値群で構成 |
| コース状態 | 下書き,レビュー状態,公開 | コースの情報ではないので分ける |
| 動画レッスン | URL,再生時間 | |
| テストレッスン | 問題集 | |
| コースレビュー | レビューテキスト | 内容のバリデーションが必要 |
エンティティ
| エンティティ | 識別子 | 主な属性・関連 |
|---|---|---|
| コース情報 | コース情報ID | コース詳細, コース状態 |
| コース | コースID | 動画レッスン, テストレッスン |
| コースコメント | コースコメントID | コメント,受講生ID,日時 |
| 受講生 | 受講生ID | |
| 講師 | 講師ID | |
| 修了証 | 修了証ID | コースID, 終了日 |
集約
コースという集約において、講師と受講生では領域が異なる。共通部分をコース集約にまとめて、各々しか関係ない領域は各々がIDを所有する仕組みが良いと考える。
| 集約ルート | 含まれるもの | 整合性のルール |
|---|---|---|
| コース | コース,コース情報ID,コースコメントID | |
| 受講生 | 受講生,コース,修了証ID | |
| 講師 | 講師,コース |
ドメインイベント
- コースが作成された
- コースの品質基準を判定した
- コースが購入された
- コースが終了した
- 月次になった
- コースのレビューコメントが投稿された
ドメインサービス
| サービス | 責務 |
|---|---|
| レビューサービス | 作成されたコースのレビューを行う |
| 修了証作成サービス | |
| 報酬支払いサービス | 講師に70%の報酬を支払う |
フィードバック・改善点
良くなった点
- ユビキタス言語の抽出を最初にやった(判断基準リストの手順①を実践できている)
- 集約の設計で「講師と受講生では領域が異なる」と認識できている(境界付けられたコンテキストの感覚に近い)
- ドメインイベントの数が増え、過去形で書けている
- ドメインサービスを名詞で命名できている(レビューサービス、修了証作成サービス)
- 集約間のID参照を意識できている(「各々がIDを所有する」という記述)
改善点 1: 「コース情報」と「コース」が分かれている
自分の解答:
コース情報(エンティティ)= コース詳細 + コース状態
コース(エンティティ)= 動画レッスン + テストレッスン
模範解答:
Course(エンティティ)= タイトル, 説明, カテゴリ, 価格, ステータス
└── Section → Lesson(動画/テスト)
「コース情報」と「コース」は同じ概念を分割してしまっている。1回目初級の「アカウント ≒ 顧客」と同じパターン。コースの メタ情報(タイトル等)もコンテンツ(レッスン)も、同じ Course エンティティの責務。
改善点 2: セクション(Section)が抜けている
要件に「コースは複数のセクションで構成され、各セクションには複数のレッスンがある」とあるので、構造は以下。
Course
└── Section(sectionId で識別 → エンティティ)
└── Lesson(lessonId で識別 → エンティティ)
├── VideoContent(値オブジェクト)
└── QuizContent(値オブジェクト)
要件の名詞リストアップ時に「セクション」が漏れている。
改善点 3: 「受講登録(Enrollment)」という概念が抜けている
今回の最大のポイント。受講者がコースを購入すると 受講登録 が生まれ、ここに進捗や購入価格のスナップショットが記録される。
× 受講生集約にコースを含める
→ 受講生が10コース買ったら集約が肥大化する
○ Enrollment(受講登録)を独立した集約にする
→ 受講者ID + コースID の参照だけ持つ
→ 進捗(LessonProgress)はここで管理
→ 購入価格のスナップショットもここ
「受講生がコースを受講する」という関係自体がエンティティになる パターン。2つのエンティティの間の「関係」や「行為の記録」が、独自のIDとライフサイクルを持つ場合、それ自体がエンティティになる。
改善点 4: 値オブジェクトの判断ミス
| 自分の解答 | 問題点 |
|---|---|
| コース詳細(タイトル,説明,カテゴリ,価格) | これらは Course エンティティの属性。セットにする意味が薄い |
| コースレビュー → 値オブジェクト | レビューはIDで追跡し、1コース1件の制約を管理する必要がある → エンティティ |
逆に以下が値オブジェクトとして抜けている。
| 抜けている値オブジェクト | 判断基準のどれ? |
|---|---|
| LessonProgress(完了フラグ, スコア) | ① セット |
| Certificate(受講者名, コース名, 修了日) | ① セット + 発行後不変 |
| Rating(1〜5の整数) | ② バリデーション |
| PassingCriteria(80%) | ② バリデーション(ポリシーの明示化) |
| Money(数値, 通貨) | ③ 計算 |
改善点 5: 集約に他の集約のオブジェクトを含めている
自分の解答:
受講生集約 = 受講生 + コース + 修了証ID
講師集約 = 講師 + コース
模範解答:
Student集約 = Student 単体
Instructor集約 = Instructor 単体
Enrollment集約 = Enrollment + LessonProgress(受講者×コースの関係)
受講生集約に「コース」を含めると、受講生のプロフィール変更時にコースの進捗データもロックされる。「一緒に変更されるか?」 で考えると、受講生のプロフィール変更と学習進捗の更新は別の操作なので、分離すべき。
集約間はIDの参照のみ。オブジェクトを直接含めない。
改善点 6: ドメインイベント
自分の解答: 模範解答との差分:
コースが作成された ○ CourseCreated
コースの品質基準を判定した → CoursePublished / CourseRejected に分ける
コースが購入された ○ CoursePurchased
コースが終了した ○ CourseCompleted
月次になった → 時間の経過はドメインイベントではない
コースのレビューコメントが投稿された ○ ReviewPosted
✗ LessonCompleted が抜けている
✗ CertificateIssued が抜けている
✗ QuizPassed / QuizFailed が抜けている
- 「品質基準を判定した」は結果が2通り(公開 or 差し戻し)あるので、結果ごとに別イベント にする方がトリガーとして使いやすい
- 「月次になった」は時間の経過であり、ビジネス上の出来事ではない。スケジューラのトリガーであってドメインイベントではない