はじめに
最近DDD勉強しました。
が、本読んでから実際にやってみよう、とするまでにちょっと時間が空いちゃいました。
色々理解して覚えながら読んでたのに記憶から大体ぶっ飛んでたので、後から読み返した部分をまとめようと思います。
ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本の文言やサンプルコードを引用した上で自分なりにまとめます。
自分用の備忘録ですが、ちゃんと人に見られるように気をつけて書いたのでたまに見に来て下さい。
値オブジェクト(Value-Object)
class User
{
public UserId id { get; set; }
public UserName name { get; set; }
}
のようなUserクラスがある時のUserId、UserNameにあたるオブジェクトです。
この時、nameはUserNameを参照型として宣言しています。
UserNameのコードは以下のようなものです。
// UserNameは値オブジェクト
class UserName
{
private readonly string value;
public UserName(string value) {
if(value == null) throw new ArgumentNullException(nameof(value));
this.value = value;
}
}
値オブジェクトの特徴としては以下があります。
- 不正な値を存在させないためのルールを持つ
UserNameでは、nullではない文字列が値として定義されることに対して強制力を持っています。
このように値オブジェクトは、その値が持つ特有のルールを表現します。 - ライフサイクルを持たない
値オブジェクトは一度生成されると、変更するためのメソッド(User.name.change('changed value')みたいなやつ)や削除するためのメソッド(User.name.delete())等を持ちません。
あくまで値オブジェクトは、値を表現するためのみに用いられます。
値オブジェクトを存在させるメリット
ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本から引用します。
- 表現力を増す
- 不正な値を存在させない
- 誤った代入を防ぐ
- ロジックの散在を防ぐ
エンティティ
エンティティは値オブジェクトとは違い、属性ではなく同一性によって識別されます。
例えば、
class Task {
public TaskName name { get; set; }
public TaskContent content { get; set; }
}
のようなTaskクラスが存在するとき、以下の二つの値オブジェクトは異なるものであると言えます。
task1 // { name: "A", content: "AAA" }
task2 // { name: "B", content: "BBB" }
なぜならTaskはアイデンティティを持たないため、それぞれの属性が同一の値であるかで識別するしかありません。
しかし、Taskがもし以下のような特徴を持っていればどうなるでしょうか。
- TaskはDBや、何かしらの永続領域に登録される
- Taskは永続化(登録)される際、ユニークなidを採番される
- Taskはname,contentを変更し、上書き保存することが可能である
この場合、Taskはname,contentが異なっていても、idが同じであれば同じデータであると認識されると思います。
この場合のTaskは値オブジェクトではなくエンティティとなります。
またドメインオブジェクトと違い、ライフサイクルを持つようになりました。
エンティティはライフサイクルを持ちます。
ドメインオブジェクト
ドメインオブジェクトは、値オブジェクトやエンティティ等のドメインモデルを表現したオブジェクトです。
値オブジェクトやエンティティといったドメインオブジェクトをコード上で表現するメリット
ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本から引用します。
- コードのドキュメント性が高まる
- ドメインにおける変更をコードに伝えやすくする
コードのドキュメント性が高まる
class User
{
public UserId id { get; set; }
public UserName name { get; set; }
}
class UserName
{
private readonly string value;
public UserName(string value) {
if (value == null) throw new ArgumentNullException(nameof(value));
if (value.Length < 3) throw new ArgumentException("😱");
this.value = value;
}
}
のように実装されていると、Userオブジェクトのnameは少なくともnullではない三文字以上の文字列であることがわかります。
しかしこれが以下のように実装されているとどうでしょうか。
class User
{
public int id { get; set; }
public string name { get; set; }
}
格段に読み取れる情報量が減りました。
数日後の自分や、他の開発者がこのコードを見たときにはきっとしんどいです。
値オブジェクトやエンティティといったドメインオブジェクトをコード上で表現することで、コードを理解しやすくなります。
ドメインにおける変更をコードに伝えやすくする
例えば
class User
{
public int id { get; set; }
public string name { get; set; }
}
のようなUserオブジェクトがあるとき、
nameは三文字以上でnullではない文字列である必要があるとします。
登録処理や上書き保存処理を行うたびに
string name = "🐱";
User user = new User();
user.id = 0;
user.name = name;
if (user.name == null) throw new ArgumentNullException(nameof(value));
if (value.Length < 3) throw new ArgumentException("😱");
みたいなことやることになると思います。
これが一箇所のみなら良いのですが、もし複数箇所に上のような実装があった場合、
nameは3文字ではなく、5文字以上にする必要が出たような場合(ドメインのルールに変更があった場合)に、上のコードの全ての出現箇所を改修しなければいけません。
一つの目的で複数箇所を改修することがまず嫌です。
(漏れが出る可能性がある、他の機能に影響が出る可能性を考慮する必要がある)
しかし、オブジェクトのルールとして値オブジェクトを定義していた場合、その必要はありません。
class User
{
public UserId id { get; set; }
public UserName name { get; set; }
}
class UserName
{
private readonly string value;
public UserName(string value) {
if (value == null) throw new ArgumentNullException(nameof(value));
if (value.Length < 3) throw new ArgumentException("😱");
// 改修箇所はここだけで済む!!!
this.value = value;
}
}