概要
ドメイン駆動設計について学んだことをまとめます。
より具体的にイメージを持ってもらうために、サンプルコードはTypeScriptで記述しました。
ドメイン駆動開発とは
用語説明
- ドメインとは対象とするシステム・サービスの領域のこと
- 対象とするシステムによって大きく異なる
- 例) 動画配信システムなら、プレイヤー、動画、など
- モデルとは現実の概念や事象を抽象化した概念
- モデルにする作業をモデリングという
- ドメイン概念をモデリングして得られたモデルをドメインモデル
- ドメインモデルをコードに落とし込んだものをドメインオブジェクト
ドメイン駆動開発
- 業務上の知識(ドメイン概念)をソースコード(ドメインオブジェクト)落とし込む開発手法
- メリット
- ドキュメントがなくてもソースコードを見れば仕様が把握がある程度可能
- ドキュメントを書く数が減る
- 可読性の向上
- 変更に強くなる
- どうすれば?
- 利用者を取り巻く世界を知ることが大事
以降、ドメイン駆動開発の手法についてまとめる
値オブジェクト
- システム固有の値はプリミティブ型(number 型, strig 型など)で表現することはできない
- 値をオブジェクトとして管理すること(値オブジェクト)でシステム固有の値として保持する
// FullNameという値をオブジェクトとして扱う
class FullName {
private readonly lastName: string;
private readonly firstName: string;
constructor(lastName: string, firstName: string) {
this.lastName = lastName;
this.firstName = firstName;
}
}
値の性質
- 1.不変である
- 2.交換が可能である
- 3.等価性によって比較される
1. 不変である
- 途中で値の変更を許すと予期せぬ時に書き変わってバグを生む
- 値自身が自身を変更する振る舞いをもつのはおかしい
- 値を変更したい場合、新たにインスタンスを生成する
// bad
const fullName = new FullName("yugo", "kashima");
fullName.ChangeLastName("sato");
2. 交換が可能である
- 値はそれ自身は変更できないが交換は可能でさる
- しかし、fullName の値が再代入されるため、バグの元になるためあまりしないやらない方が良い?
let fullName = new FullName("yugo", "kashima");
fullName = new FullName("yuji", "kakimoto");
3. 等価性によって比較される
- 値オブジェクトは値なので、以下のように値の値を取り出して比較するコードは不自然
- 下のようにすると例えば、ミドルネームというインスタンスプロパティが追加されても修正は
equalsメソッドだけで済む
// bad
const userA = new FullName("yugo", "kashima");
const userB = new FullName("yugo", "kashima");
if (userA.lastName === userB.lastName && userA.firstName === userB.lastName) {
console.log("同一");
}
// good
class FullName {
// 略
public equals(other: FullName): boolean {
// 比較する
}
}
const userA = new FullName("yugo", "kashima");
const userB = new FullName("yugo", "kashima");
userA.equals(userB);
業務上のルールを値オブジェクトに含める
名前という値オブジェクトを考える。業務上のルールとして名前は 1 文字以上で指定するということがあるとする。
以下のようにするとコードを見るだけで、ルールがわかる。
// FullNameという値をオブジェクトとして扱う
class FullName {
private readonly lastName: string;
private readonly firstName: string;
constructor(lastName: string, firstName: string) {
if (lastName === "") throw Error("苗字は1文字以上で指定する必要があります");
if (firsttName === "")
throw Error("名前は1文字以上で指定する必要があります");
this.lastName = lastName;
this.firstName = firstName;
}
}
どこまで値オブジェクトにすべきか
- 以下2点を基準に値オブジェクトにする
- ルールが存在するか
- それ単体で取り扱いたいか
値オブジェクトに振る舞いを持たせる
- オブジェクトに対する処理を振る舞いとして一処にまとめることで、自身に関するうルールを語るドメインオブジェクトらしさを帯びる
- 変更が容易
- ドキュメント不要
- 値に関する処理は全て値オブジェクト内に書く
class Money {
private readonly value: number;
purblic add(other: Money) {
const sum = other.value + value;
return new Money(sum); // 値は普遍尾ため新たにインスタンスを定義して返す
}
}
値オブジェクトを利用するメリット
- 1.表現が増す
- 2.不正な値を存在させない
- 3.誤った代入を防ぐ
- 4.ロジックの散在を防ぐ
1. 表現が増す
- string 型や number 型では文字列、数値であれば何でも許容するため、文字列、数値という意味しかもたない
- 業務上で扱う値は、必ず何かしらのルールや、意味を持つ
- 値オブジェクトにルールを記載するので、仕様の代わりになり、何を表しているのかが明確になる
// bad
const productCode = "a-132-2012";
// good
class productCode {
private readonly area: string;
private readonly auth: string;
private readonly createdYear: string;
}
2. 不正な値を存在させない
- システムで扱う値は必ずルールが存在する
- 固定の電話番号は 10 桁
- ユーザーは 3 文字以上 10 文字以下
- メールアドレスはアルファベトのみ、任意の文字列@ドメイン名
- プリミティブの strig 型だと文字列であれば何でも許容される
// 11であるが許容される
const phoneNumber = "09232322344";
- 値を利用する色んな箇所でチェックする必要がある
- 仕様変更になった時に全てを修正する必要があり大変
if (phoneNumber.length === 10) {
// 正しい挙動の処理
} else {
throw new Error("電話番号が不正です");
}
3. 誤った代入を防ぐ
- プリミティブ型で扱うとエラーが出ずにバグの原因となる
- id と name はそれぞれ値オブジェクトにすると良い
// エラーが出ない
function createUser(name: string) {
const id = name;
return new User(name, id);
}
4. ロジックの散在を防ぐ
- インスタンスプロパティをそのまま返す getter を用意することは外部に同じようなロジックが散財する可能性がある
- setter も同様
- setter, getter を利用したい婆は参照先のロジックが値オブジェクトに移動できないか検討
- なるべく getter, setter は利用しない
まとめ
- 値オブジェクトはシステム固有の値を作ること
- システムで取り扱う値には必ずルールがあるため、プリミティブ型で取り扱わないようにする