はじめに
QUOカード デジタルイノベーションラボのコーディングテスト記事を題材に、DDD(ドメイン駆動設計)を意識した設計書サンプルをまとめました。設計方針についてはこの記事を参照してください。
本記事では同記事を参考にさせていただき、必要最小限の機能だけを設計に反映しています。実装詳細は扱いません。
ドメイン概要
ドメインは、著者(Author)と書籍(Book)を管理する。提供する能力は 著者の登録・更新、書籍の登録・更新、および 特定の著者に紐づく書籍一覧の取得。著者と書籍は多対多の関係で、書籍は1名以上の著者を必ず保持する。
用語定義(ユビキタス言語)
以降の説明で言葉の意味がぶれないように、主要な用語を明確化します。モデル名とAPIの語彙を揃えると、読み手の誤解を減らせます。
用語 | 説明 |
---|---|
Author | 著者エンティティ |
Book | 書籍エンティティ |
AuthorId | 著者ID(値オブジェクト) |
BookId | 書籍ID(値オブジェクト) |
Status | 出版状況(未出版/出版済み) |
ユースケース
ユーザーが何をしたいかを短く列挙します。ここでの範囲がAPIとテーブル設計の境界になります。更新・削除は仕様に記載がないため対象外です。
- 著者を登録する/更新する
- 書籍を登録する/更新する
- 特定の著者に紐づく書籍一覧を取得する
ドメインモデル図
関係はシンプルです。著者1 : 書籍多の関係を基本とし、書籍は最低1名の著者を必須とします。
Author 1 --- * Book
この図は「どの情報がどこに属するか」を共有するための最小粒度の表現です。
エンティティ・値オブジェクト
不変条件を守りたい値(変わってはいけないルールを持つ値)はVO(値オブジェクト)に、同一性で扱うもの(誰なのか、どれなのかを識別するもの)はエンティティに置きます。
この違いを意識して設計することで、モデルがより明確になり、扱いやすくなります。
Author
- authorId: AuthorId
- name: String
- dateOfBirth: LocalDate(現在より過去のみ)
Book
- bookId: BookId
- title: String
- price: Integer(0以上)
- authors: List(1名以上必須)
- status: Status(未出版/出版済み、出版済み→未出版への変更不可)
API仕様
ユースケースをそのままエンドポイントに落とし込むイメージです。パスや命名はユビキタス言語に合わせています。
メソッド | パス | 機能 |
---|---|---|
POST | /api/authors | 著者の登録 |
PUT | /api/authors/{id} | 著者の更新 |
POST | /api/books | 書籍の登録(著者1名以上・価格0以上) |
PUT | /api/books/{id} | 書籍の更新 |
GET | /api/authors/{id}/books | 著者に紐づく書籍一覧の取得 |
バリデーション・制約
ドメインの健全性を保つ最小限のルールに絞ります。エラー内容はAPI側で適切に返せるよう、チェック箇所をドメインに寄せるのが基本です。
- 著者名:空文字は不可(100文字以内など)
- 生年月日:未来日は不可(現在より過去のみ)
- 書籍タイトル:空文字は不可
- 価格:0以上
- 著者リスト:1名以上必須
- ステータス:出版済み → 未出版 への巻き戻しは禁止
テーブル設計
著者と書籍は多対多(1冊に複数著者/1人が複数冊)なので、中間テーブルで結びます。DBでは必要最低限の制約だけを持たせます。
authors(著者)
カラム名 | 型 | 制約 |
---|---|---|
author_id | BIGINT | PK |
name | VARCHAR(100) | NOT NULL |
date_of_birth | DATE | NOT NULL |
最小制約
- nameは必須
- date_of_birthは過去日
books(書籍)
カラム名 | 型 | 制約 |
---|---|---|
book_id | BIGINT | PK |
title | VARCHAR(255) | NOT NULL |
price | INTEGER | NOT NULL(0以上) |
status | VARCHAR(20) | NOT NULL(列挙型) |
最小制約
- titleは必須
- price >= 0
- statusは許可値のみ(未出版、出版済み)
books_authors(中間)
カラム名 | 型 | 制約 |
---|---|---|
book_id | BIGINT | FK(books) |
author_id | BIGINT | FK(authors) |
最小制約
- 両カラムは既存レコードを参照(外部キー必須)
- (book_id, author_id) を一意(重複紐付け防止)
テスト方針
以下の内容でテストを作成します(名称は統一して「ドメイン」「API」「DB」)。
ドメイン(単体)
- Book
- price >= 0 を満たさないとエラー
- titleが空だとエラー
- authorsが1名未満(空配列)だとエラー
- statusは
UNPUBLISHED → PUBLISHED
のみ許可(PUBLISHED → UNPUBLISHED
は不可)
- Author
- nameが空だとエラー
- date_of_birthは現在より過去でないとエラー
API(最小経路)
- 正常系
POST /api/authors
で著者を1件作成POST /api/books
で上記著者IDを含めて書籍を作成(price >= 0、status = UNPUBLISHED)GET /api/authors/{id}/books
で当該書籍が取得できることPUT /api/books/{id}
でUNPUBLISHED → PUBLISHED
に更新できること
- 異常系(代表)
POST /api/books
でauthors
空/price < 0
/title
空 → 400系POST /api/books
で 存在しない authorId を指定 → 400/409系PUT /api/books/{id}
でPUBLISHED → UNPUBLISHED
を試みる → 400/409系
DB
- 外部キー違反で挿入が失敗すること
(books_authors.author_id に存在しないIDを入れるなど) - 一意制約:books_authors (book_id, author_id) の重複挿入が失敗すること
まとめ
記事に示された機能要件(登録・更新/著者→書籍の取得)、および 属性制約(価格0以上、著者1名以上、出版済みの巻き戻し不可、生年月日は過去) に一致する設計を、DDDに沿って整理しました。この記事がDDDを勉強している方の参考になれば幸いです。
コメント