[3]エンティティを識別する属性と振る舞いを検討
続いて、エンティティに注目してモデリングを行います。エンティティ設計の初期段階では「エンティティを一意に特定する属性と振る舞い」にだけ注目します。
同時に用語集を作成し、ユビキタス言語について認識を合わせます。ドメインエキスパートとして業務が正しく表現できているか、開発者としてそのまま実装できるかを確認していきます。
[4]「一意な識別子」の設計
そして、エンティティを識別する「一意な識別子」を検討します。SaaSOvationでは、テナントは「テナントID」と「テナント名」、ユーザーは「テナントID」と「ユーザー名」を一意な識別子としています。テナントの場合、システム的には「テナントID(UUID)」にて一意性を保証します。利用者の視点では「テナント名」を使って問い合わせを行い識別します。
なお、テナントIDは別の境界づけられたコンテキストを含む他のオブジェクトから参照されるため、独立した「値オブジェクト」として設計しています。
一意な識別子の生成パターン
一意な識別子を決定したら、一意な識別子の生成方法について検討します。IDDD本では一意な識別子の生成方式として以下の4パターンを提示しています。一意な識別子というと難しく感じるかもしれませんが、IDや主キー値の生成方法といえば、イメージしやすいと思います。
| No | 概要 | 値の例 | メリット | デメリット |
|---|---|---|---|---|
| 1 | ユーザーが「入力」 | USR123 | 識別子を生成する仕組みが不要 | 重複せず適切な識別子を利用者に登録してもらうワークフローやチェックの仕組みが必要 |
| 2 | アプリケーションが「生成」 |
"912649a0-0ed2-4071-8af6-1a350d4adbd0" (UUIDのみ)や |
UUID(GUID)の生成アルゴリズムを用いて一意な値を軽量に生成 | 人間には理解しがたい文字列(そのため、通常は利用者には非表示) |
| 3 | 永続化メカニズム(DB)が「生成」 | 10001 | Oracleのシーケンス等の仕組みを用いて連番を簡単に生成 | DB依存が高く性能面の課題がある(早期生成時におけるキャッシュの仕組みの検討が必要) |
| 4 | 他の境界づけられたコンテキストから「割り当て」 | USR123やUUID等 | 識別子生成の仕組みを他のコンテキストに託せる | 他のコンテキストに対して識別子を取得するための検索/マッチング/割り当ての仕組みが必要 |
エンティティに適した生成方法を選択します。SaaSOvationの「ユーザーエンティティ」では、テナントIDは「アプリケーションでの生成(UUID生成)」を行い、ユーザー名は「ユーザーが入力」という形をとっています。下図はそのコード例です。
一意な識別子の生成タイミング(早期、遅延)
次に、一意な識別子の生成タイミングについて紹介します。生成タイミングについては、「早期生成」と「遅延生成」の2パターンが存在します。上図のコードの通り、TenantIDでは、リポジトリ(12章)を用いて「早期生成」を行っています。
| No | 概要 | メリット | デメリット |
|---|---|---|---|
| A | 早期生成(オブジェクト作成時に生成) | オブジェクト作成と同時に識別子を使用可能 | DBから識別子を生成する場合は、オブジェクト生成時にリポジトリ経由でDBから識別子を取得する必要がある |
| B | 遅延生成(オブジェクト保存時に生成) | 昔ながらの生成方法で、馴染みやすい | オブジェクト生成時にドメインイベントを発行させる場合に、一意な識別子が存在しない。またオブジェクト生成時に識別子が未設定(nullや0)のためオブジェクト間の比較に失敗する |
一意な識別子で、エンティティの同一性を判定(equals/hashCodeメソッド)
エンティティが同じかという「同一性」を判定する場合、一意な識別子を使ってオブジェクト同士を比較します。IDDDでは下図のコードのように、equalsメソッドとhashCodeメソッドをオーバーライドして、一意な識別子の値の組み合わせが同じであれば、同じエンティティであるとみなしています。
従来のシステム開発ではDB保存時にIDを生成する「遅延生成」が多かったと思います。しかし遅延生成の場合、一意な識別子にデフォルト値(nullや0)が設定されているため、異なるオブジェクトが同一と判定されてしまう問題があります。この問題に対応するには、遅延生成をやめるか、早期生成でも問題ないようにequalsメソッドとhashCodeメソッドをオーバライドして、識別子以外で同一性を判断できるようにする必要があります。
一意な識別子を変更しない「不変性」
注意点として、一意な識別子を設定した後は、その識別子の値を変更しないようにします。もし、識別子が設定済みにもかかわらず、変更処理が呼び出された場合はエラーとなるようにしなければいけません。通常は、一意な識別子のアクセサメソッド(セッター)にアサーションを記載します。
O/Mマッパー用のID項目「代理識別子」
なお、最近の実装ではDBにアクセスするライブラリとして、HibernateやEntity FrameworkといったO/Rマッパーを使用することも多いと思います。「代理識別子」とは、O/Rマッパーが使用するシステム的な識別子のことです。
「レイヤスーパータイプ」で共通処理を実装
代理識別子(システム上のキー)は一意な識別子(ビジネス上のキー)ではないため、外部から見えないようにしておくことが望ましいです。代理識別子を外部から見えなくする方法として、レイヤ共通の処理を記述する「レイヤスーパータイプ」を導入することができます。下図のコードでは、エンティティの親クラス(レイヤスーパータイプ)で、idという代理識別子を提供し、可視範囲をProtectedで絞り込み、外部から見えないようにしています。
このように共通処理を適切に管理できる場合は、レイヤスーパータイプを導入するとよいでしょう。下図は、同一性を判断するメソッドをレイヤスーパータイプで共通化する例です。
レイヤスーパータイプで同一性に関する処理を共通化(C#)
