エンティティとは
「エンティティ」という言葉はシステム開発をしている方なら、一度は聞いたことがあると思います。
データベースのER図で表現される「エンティティ」や、ORMなどにデータを格納するオブジェクトを「エンティティ」と呼んだりします。
しかし、ドメイン駆動設計における「エンティティ」は上記に記載した それらとは異なる ことを注意してください。
まず、「エンティティ」はドメインオブジェクトの一つです。
前回の記事で紹介した通り、「値オブジェクト」もドメインオブジェクトの一つです。
「エンティティ」と「値オブジェクト」の違いは同一性によって識別されるかどうかです。
以下で具体的に見ていきましょう。
前回の記事でも扱った「氏名」という属性は固定値ではなく、とある理由により変化します。
例えば、結婚をした際、姓が変わることが考えられます。
ここで注目してほしいのは、仮に「山田太郎」が「鈴木太郎」に氏名が変わったとして、その人自体が別人に変わってしまうかというところです。
確かに、名前は変わっていますが、名前が変わったからといってその人が全く異なる人になることはありません。
同様に、その人が歳を取っても、体重が増減しても、その人がその人であるということは揺ぎようがありません。
以下、具体例です。
山田太郎さんは、1年経つ間に少し太ってしまったみたいです。
しかし、当たり前ですが太郎さんはずっと太郎さんです。
この比較の考え方が同一性であるかどうかです。
上記の図、「エンティティ1」と「エンティティ2」の属性は違えど、同一の人を表現しているはずです。
もっと言えば、属性で同一性を区別されないと言えるでしょう。
ソフトウェア開発をしたことがある人は、必ずと言ってもいいぐらい「ユーザ」という概念を利用したことがあると思いますが、これがまさしく「エンティティ」として扱われます。
システムの利用を開始する際、最初にユーザ情報として属性(ユーザ名、メールアドレス、電話番号など)を登録します。
これら属性は基本的に登録後に修正可能であることが多いです。
ただ、これらの属性が変わったとして、ユーザが変わったことになるでしょうか?
ユーザはそのままそのシステムで利用していた内容やデータを変わらず使い続けることができます。
これはユーザが属性ではなく、同一性によって、識別されていると言えるでしょう。
少し周りくどくなってしまいましたが、この同一性によって区別されるドメインオブジェクトのことを「エンティティ」と呼びます。
エンティティの性質
冒頭でも少し触れましたが、「エンティティ」「値オブジェクト」はどちらも「ドメインオブジェクト」です。
これらは似通っていますが、性質は異なっています。
順番に見ていきます。
理解を深めるために
本書には、「値オブジェクト」と「エンティティ」の違いを確認しながら、これから紹介する性質を読み進めると、理解が深まると記載があります。
以下では、「値オブジェクト」との異なる点についても触れながら説明するつもりですが、不安が残るなら再度確認してみてください。
可変である
「値オブジェクト」は不変なオブジェクトでした。それに比べて、「エンティティ」は可変なオブジェクトです。
最初の具体例でも確認した通り、ユーザの属性は変化する可能性があり、「エンティティ」はこれを許容します。
ただし、可変であって、注意するべきなのは、必ずすべての属性を可変にする必要はないということです。
可変であることは、システムにおいて厄介な存在です。
可能な限り、不変にしておくというのは良い習慣だと筆者は訴えています。
同じ属性であっても区別される
「値オブジェクト」は同じ属性であれば、同じものとして扱われました。(例えば、氏名など)
エンティティはそれとは異なり、たとえ同じ属性であっても、区別されます。
「エンティティとは」では、名前が異なる場合、必ず異なるエンティティになるとは言えないことを説明しました。
逆に「名前が同じ」= 「エンティティが同一」は成立しないです。
同姓同名という人を同一の「エンティティ」としては扱うことはできないです。
システムにおいて、識別子(user_idなどのPrimaryKey)を利用して同一性を確認するのは、ごく当たり前のことです。
同一性をもつ
「値オブジェクト」は同一性を持ちません。 持っているのは等価性です。
ユーザは同一性を持っているので、属性が変わっても、変更前と変更後でユーザを同一のユーザとして識別する必要があります。
「同じ属性であっても区別される」でも説明した通り、システム上で同一性を判断するための実態は「識別子」です。
とあるユーザ情報A, Bが一致するかどうかを確認するには、ユーザ名、メールアドレスなどの属性ではなく、識別子のみを比較します。
import java.util.Objects;
public class User {
// UserIdは値オブジェクトとして扱う
private final UserId userId;
private String userName;
private String email;
public User(UserId userId, String userName, String email) {
this.userId = Objects.requireNonNull(userId);
this.userName = userName;
this.email = email;
}
// ユーザIDによる等価比較
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof User)) {
return false;
}
User user = (User) o;
// ★★★ 比較は識別子(UserId)のみを比較する ★★★
return userId.equals(user.userId);
}
@Override
public int hashCode() {
return Objects.hash(userId);
}
}
エンティティの判断基準は「ライフサイクル」、「連続性」
ドメインオブジェクトの定義の際に「値オブジェクト」として定義するか、「エンティティ」として定義するかは「ライフサイクル」と「連続性」が判断基準となります。
ユーザであれば、利用者によって作成されます。
システム利用の中でユーザ名などを変更することもあるでしょう。
年月が経ち、利用者にとってシステム利用が不要になった際、ユーザ情報は削除されます。
ユーザはライフサイクルをもち、連続性がある概念です。これは「エンティティ」として扱えると言えそうです。
値オブジェクト or エンティティ
時には同じ物事でも、「値オブジェクト」と「エンティティ」の両方の側面をもつ場合があります。
車にとってタイヤは部品です。タイヤそれぞれには特性があったりするものの、交換可能なものです。
これは「値オブジェクト」として表現ができそうです。
しかし、タイヤを製造する側からみた場合、どうでしょう。
タイヤを製造し、それらを識別することは重要です。作成前~出荷後にそのタイヤを識別する必要も出てくるでしょう。
その際は、「エンティティ」として扱う必要が出てきそうです。
同じ概念を取り扱う場合でも、それを取り巻くドメインによってモデルに対する捉え方は変わってきます。
「値オブジェクト」と「エンティティ」のどちらにもなり得る概念が存在することを意識しておくことが必要だと記載があります。
ドメインオブジェクトを定義するメリット
改めて「値オブジェクト」や「エンティティ」などのドメインオブジェクトを定義するメリットにはどのような物があるでしょうか。
これから紹介するメリットは初期の製造工程よりも、その後の保守開発において役立つものです。
コードのドキュメント性が向上
開発現場において、設計書が細かい仕様を網羅していないことがしばしばあります。
そんなとき、あなたは以下のようなコードを見つけたとします。
public class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
上記のようなコードでは、良い情報を得ることはできません。
しかし、以下のようなコードであればどうでしょうか。
public class UserName {
private final String value;
public UserName(String value) {
if (value == null) {
throw new IllegalArgumentException("ユーザ名は null を許容しません。");
}
if (value.length() < 3) {
throw new IllegalArgumentException("ユーザ名は3文字以上である必要があります。");
}
this.value = value;
}
public String getValue() {
return value;
}
(略)
}
上記のUserNameクラスを確認すれば、UserNameはnullを許容せず、3文字以上は必要であることが分かります。
これは、開発者にとってうれしいことです。
UserNameの条件がUserNameクラスにまとまっており、ルールが明示的に記載されているのです。
つまり、コードのドキュメント性の向上につながるのです。
さらに言えば、上記のチェック、バリデーションを他のクラスに記載しなければ、UserNameというドメインオブジェクトに自身のルールが凝縮することになります。
DRY原則が守られ、ロジックの分散を防ぐことにも繋がるのだと思います。
また、これから紹介する利点にも関わってくると思います。
ドメインにおける変更をコードへ伝えやすくなる
ドメインオブジェクトにルールをまとめておくと、ドメインにおける変更をコードに伝えやすくなります。
どういうことでしょうか。
例えば、「ユーザ名は最小3文字」というルールが「ユーザ名は最小5文字」になった場合を考えます。
UserNameという値オブジェクト(ドメインオブジェクト)として扱っていなければ、開発者はどこにチェックロジックが眠っているか、プロジェクトソース全体を探すことになります。
しかし、先ほど提示したUserNameを利用して、ドメインのルールをUserNameにまとめられていたらどうでしょうか。
UserNameのクラスを確認し、修正すれば完了します。(もちろん、DDDが順守されている前提)
ドメインオブジェクトを使うことで、ルールが記載されている箇所が明確で、その調査、修正も容易いでしょう。
結果的にドメインの変更点をソースに伝えやすくなるのです。
Chapter3の実践
さて、実践!
一覧記事の方に、題材とする機能を記載しているので、こちらに沿って今回学んだエンティティをソースに落とし込んでいきます。
Chapter2で先に答えを記載しましたが、今回はユーザ、サークルをエンティティのオブジェクトとします。
同記事の「エンティティとは」でも紹介した通り、ユーザはライフサイクルのあるドメインオブジェクトです。
Aさんが成長して身長が伸びても、少し食べすぎて太っても、月日がながれて年齢を重ねても、Aさんに変わりありません。
つまり、エンティティとして定義が可能です。
サークルはどうでしょうか。
イメージしてください。とある大学のサークルCは30年続いているサークルだとします。
30年の中で、人が入れ替わり、体制が変わることがあるでしょう。やっていた競技がサッカーだったのに、いつの間にかボードゲームサークルになっているかもしれません。。。(?)
しかし、サークルCは30年の歴史があり、中身が変わろうがサークルCです。
今回はサークルもエンティティとして扱うことにします。
まずはCircleエンティティの実装から紹介します。
ソースコードについて
Qiitaの記事で説明する都合上、エラーメッセージがベタ書きとなっています。
本来MessageConst.javaのようなクラスに切り出したいのですが、ファイル単位でいくつもソースがあると分かりづらそうなので、ご容赦ください。
/**
* サークルエンティティ
*/
public class Circle {
/** サークルID(値オブジェクト) */
private final CircleId id;
/** サークル名(値オブジェクト) */
private CircleName name;
/** サークルオーナー */
private User owner;
/** サークルメンバー */
private List<User> members;
/**
* コンストラクタ
* @param id サークルID
* @param name サークル名
* @param owner サークルオーナー
* @param members サークルメンバーリスト
*/
public Circle(CircleId id, CircleName name, User owner, List<User> members) {
if (id == null) {
throw new IllegalArgumentException("CircleId は null にできません");
}
if (name == null) {
throw new IllegalArgumentException("CircleName は null にできません");
}
if (owner == null) {
throw new IllegalArgumentException("owner は null にできません");
}
if (members == null) {
throw new IllegalArgumentException("members は null にできません");
}
this.id = id;
this.name = name;
this.owner = owner;
// 外部から渡されたリストをコピーして保持(外部変更を防止)
this.members = new ArrayList<>(members);
}
/** getter */
public CircleId getId() {
return id;
}
public CircleName getName() {
return name;
}
public User getOwner() {
return owner;
}
/** メンバーリストは変更不可で取得する */
public List<User> getMembers() {
return Collections.unmodifiableList(members);
}
public void setName(CircleName name) {
if (name == null)
throw new IllegalArgumentException("CircleName は null にできません");
this.name = name;
}
public void setOwner(User owner) {
if (owner == null)
throw new IllegalArgumentException("owner は null にできません");
this.owner = owner;
}
public void setMembers(List<User> members) {
if (members == null)
throw new IllegalArgumentException("members は null にできません");
this.members = new ArrayList<>(members);
}
/** エンティティの等価性は CircleId で判断 */
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Circle)) {
return false;
}
Circle other = (Circle) obj;
return id.equals(other.id);
}
@Override
public int hashCode() {
return id.hashCode();
}
}
Circleクラスでは、以下で構成されています。
- サークルを識別するID
- サークル自身の名前
- サークルの代表を表現するオーナー
- サークル所属者を表すメンバー
CircleIdはそのサークルを表現する識別子なので、setterは定義しません。
IDが変わるのはそのサークル自体が別のサークルに変わることを指します。これは現実世界ではありえないことです。
PG内では、setterを定義して、別のサークルとして扱うことは可能ですが、バグの元となるでしょう。
その他の属性値は可変なので、setterを定義しています。
また、同一性を確認するためにequalsメソッドをオーバライドします。
実装では、各属性すべてが同一かチェックするのではなく、キーとなるCircleIdの値のみで確認するのがポイントだと思います。
※しつこいですが、ここでサークル名などで比較をすると、連続性のあるエンティティの比較として不適切です。
なぜならば、サークル名は途中で変わっている可能性があるからです。また、同一名のサークル名の別サークルがあるかもしれません。サークルの同一性を区別する役割を持っているのは識別子のCircleIdです。
ユーザのエンティティ実装
サークルよりも単純で、以下の属性を持つように実装します。
- ユーザを識別するID
- ユーザ自身の名前
また、ユーザIDも値の変更は許可しません。
/**
* Userエンティティ
*/
public class User {
/** ユーザーID(値オブジェクト) */
private final UserId id;
/** ユーザー名(値オブジェクト) */
private UserName name;
/**
* コンストラクタ
*
* @param id ユーザーID
* @param name ユーザー名
*/
public User(UserId id, UserName name) {
if (id == null) {
throw new IllegalArgumentException("UserId は null にできません");
}
if (name == null) {
throw new IllegalArgumentException("UserName は null にできません");
}
this.id = id;
this.name = name;
}
/**
* ユーザーID取得
*/
public UserId getId() {
return id;
}
/**
* ユーザー名取得
*/
public UserName getName() {
return name;
}
/**
* ユーザー名更新
*/
public void setName(UserName name) {
if (name == null) {
throw new IllegalArgumentException("UserName は null にできません");
}
this.name = name;
}
/**
* エンティティの等価性は UserId で判断
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof User)) {
return false;
}
User other = (User) obj;
return id.equals(other.id);
}
@Override
public int hashCode() {
return id.hashCode();
}
}
以上で題材に登場するドメインオブジェクト(値オブジェクト、エンティティ)が出揃いました。
次のChapterでは「ドメインサービス」というドメインオブジェクトの振る舞いについて記述する概念をご紹介します。
次回の記事
Comming Soon...
記事一覧