Entity design
エンティティの設計(識別子・ライフサイクル・振る舞い・不変条件)と Entity / Value Object の判別を支援する。 次のような依頼があったときに使う:「エンティティを設計する」「Entityを作る」 「エンティティかValue Objectか判定する」「識別子を設計する」「IDの生成方法を決める」 「ライフサイクルをモデル化する」「状態遷移を設計する」「エンティティをレビューする」 「貧血エンティティを改善する」。 または、エンティティ、Entity、同一性、識別子、ID設計、ライフサイクル、状態遷移に言及する場合にも使う。From its SKILL.md
npx -y skills add dskst/ddd-skills --skill entity-designAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
7.8 KB, as published. Nobody here has run it
エンティティ設計
DDD におけるエンティティの設計(識別子・ライフサイクル・振る舞い・不変条件)と、Entity / Value Object の判別を支援する。
エンティティとは
属性ではなく同一性(Identity)によって区別され、ライフサイクルを通じて連続性を持つオブジェクトである。
特性:
- 同一性(Identity): 属性が変化しても識別子が同じなら同一のオブジェクトである
- 可変(Mutable): ライフサイクルを通じて状態が変化する。ただし変更は必ずビジネスメソッド経由で行う
- ライフサイクル: 生成され、状態遷移し、やがて終端状態に至る
- 不変条件の保護: 自身のビジネスルールを常に満たすよう、自分自身で守る
Entity か Value Object か
判断ツリー
このオブジェクトはEntityかValue Objectか?
├─ 属性が変わっても「同じもの」として追跡したい → Entity
├─ 時間とともに状態が変化する → Entity
├─ ライフサイクル(生成〜終了)を持つ → Entity
├─ 属性がすべて同じなら交換可能 → Value Object
├─ 履歴や監査の対象として個体を特定したい → Entity
└─ 計算・変換・検証のための値 → Value Object
判定例
| オブジェクト | 判定 | 理由 |
|---|---|---|
| 注文(Order) | Entity | 状態が変化しても同じ注文として追跡する |
| 金額(Money) | Value Object | 1000円はどの1000円でも交換可能 |
| 顧客(Customer) | Entity | 住所が変わっても同じ顧客である |
| 住所(Address) | Value Object | 値が同じなら区別する必要がない |
| 座席予約(Reservation) | Entity | 予約ごとに個体を追跡する |
| 期間(DateRange) | Value Object | 値そのもの |
注意: 同じ概念でもコンテキストによって判定が変わる。配送コンテキストの「住所」は VO だが、住所マスタ管理コンテキストでは Entity になりうる。
識別子の設計
識別子は Value Object にする
プリミティブ型(String / int)の ID は型の取り違えを許す。ID 専用の Value Object を定義する。
OrderId:
value: UUID
static generate() -> OrderId
equals(other: OrderId) -> boolean
これにより cancelOrder(customerId) のような引数の取り違えがコンパイル時に検出できる。
自然キー vs サロゲートキー
| 観点 | 自然キー(メールアドレス等) | サロゲートキー(UUID等) |
|---|---|---|
| 安定性 | 業務変更で変わりうる(リスク) | 不変 |
| 可読性 | 高い | 低い |
| 生成 | ドメインから自然に決まる | 生成戦略が必要 |
| 推奨 | 変わらないと保証できる場合のみ | 原則こちら |
自然キーが業務上意味を持つ場合は、サロゲートキーを同一性に使い、自然キーは一意制約を持つ属性として扱う。
ID 生成のタイミング
IDをいつ生成するか?
├─ ドメイン層で生成(UUID等)
│ ├─ 永続化前からIDが確定し、イベント発行や参照が容易 → 推奨
│ └─ Order.place() の時点で OrderId.generate()
│
└─ 永続化層で生成(DBの自動採番)
├─ 保存するまでIDが存在しない「未完成な」エンティティが生まれる
└─ 既存システムの制約がある場合のみ許容する
業務的な採番ルール(連番・年度プレフィックス等)が必要な場合は、採番をドメインサービスまたはリポジトリの責務として明示する。
振る舞いの設計
セッターを排除し、ビジネスメソッドを定義する
状態の変更は必ず「ドメイン上の操作」として表現する。
// NG: データ操作
order.setStatus(CANCELLED)
order.setCancelledAt(now)
// OK: ビジネス操作
order.cancel(reason)
├─ キャンセル可能か検証する(不変条件)
├─ 状態を CANCELLED に変更する
└─ OrderCancelled イベントを記録する
Tell, Don't Ask
エンティティの状態を外部で判定せず、エンティティ自身に問い合わせる・命じる。
// NG: 状態を取り出して外部で判定
if order.status == PLACED AND order.items.isNotEmpty():
...
// OK: エンティティに尋ねる
if order.canConfirm():
order.confirm()
状態遷移のモデル化
ライフサイクルを持つエンティティは、状態と遷移ルールを明示的にモデル化する。
OrderStatus: DRAFT → PLACED → CONFIRMED → SHIPPED → DELIVERED
└→ CANCELLED(PLACED までは可能)
Order.confirm():
assert this.status.canTransitionTo(CONFIRMED), "確定できない状態である"
this.status = CONFIRMED
- 遷移ルールは状態を表す Value Object(enum)に持たせるか、エンティティのメソッド内で検証する
- 不正な遷移はドメイン例外を投げる(exception-design スキルを参照)
不変条件の保護
エンティティは「常に正しい状態」を自分で保証する。
- コンストラクタ/ファクトリで生成時の不変条件を検証する
- すべてのビジネスメソッドの冒頭で事前条件を検証する
- 内部コレクションは外部に直接公開しない(読み取り専用ビューを返す)
- 複数エンティティにまたがる不変条件は集約の責務である(aggregate-design スキルを参照)
アンチパターン
| アンチパターン | 問題 | 対処 |
|---|---|---|
| 貧血エンティティ | getter/setter のみでロジックがサービスに漏れる | ビジネスメソッドをエンティティに移動する |
| 公開セッター | 不変条件を迂回して状態を変更できる | セッターを削除しビジネスメソッドに置き換える |
| プリミティブID | ID の取り違えを許す | ID を Value Object にする |
| 巨大エンティティ | 責務過多で変更が困難 | Value Object の抽出・集約の分割を検討する |
| コレクションの直接公開 | 外部から要素を追加・削除できる | 読み取り専用で返し、変更はメソッド経由にする |
| DB都合の構造 | テーブル構造をそのまま写したエンティティ | ドメインの概念で再設計し、マッピングはインフラ層で吸収する |
擬似コード例
examples/ ディレクトリに具体例がある:
examples/entity-example.pseudo— 識別子・状態遷移・不変条件を備えたエンティティの実装例