はじめに
ソフトウェア開発における設計上の問題としてよく取り上げられる「ドメインモデル貧血症」。一見、避けるべきものに思えますが、果たして本当にそうなのでしょうか?結論としては、DDDの「ドメイン知識をコードで表現すること」に反する設計状態のため、基本的に避けるべきだと考えています。この記事では、ドメインモデル貧血症の原因とその影響を紹介します。
ドメインモデル貧血症とは
ドメインモデル貧血症とは、ドメインオブジェクトに本来書かれるべきドメイン知識がドメインオブジェクトの外(サービスやコントローラーなど)に書かれてしまっている状態のことを指します。
よくGetter/Setterしかないドメインオブジェクトのことだと言われがちですが、それだけだと物足りません。
ドメインオブジェクトの外でドメイン知識が書かれてしまっているため、結果的にGetter/Setterしかない状態になってしまうというのが適切です。
以下はEmailという値オブジェクトでドメインモデル貧血症になっている例・ないっていない正しい例のコードです。
// Email値オブジェクト(ただのデータの入れ物)
class Email {
private _value: string;
constructor(value: string) {
this._value = value;
}
get value(): string {
return this._value;
}
}
function createUser(emailStr: string) {
// 呼び出し側(サービスやコントローラー)でバリデーションなどを行っている
if (!emailStr.includes('@')) {
throw new Error('Invalid email address');
}
const email = new Email(emailStr);
// ユーザー作成処理へ進む…
}
class Email {
constructor(private readonly _value: string) {
if (!this._value.includes('@')) throw new Error('Invalid email address');
}
get value(): string {
return this._value;
}
}
// 呼び出し側(サービスやコントローラー)でバリデーションなどを行っている
function createUser(emailStr: string) {
const email = new Email(emailStr);
// ...
}
ドメインモデル貧血症の問題点
ドメイン知識の欠如
ドメインモデルが単なるデータ保持クラスに成り下がり、ビジネスルールやドメイン固有の知識が外部(サービス層など)に分散してしまいます。これにより、ドメインモデルがビジネスの意図を反映していないため、ドメインの深い理解が得られません。ドメインの深い理解がドメインモデルに表現されないというDDDの目的に反してしまいます。
凝集度の低下
ドメインモデルにロジックが持たれないため、データとロジックが別々の場所に分散し、凝集度が低くなります。これにより、同じデータに関連する処理があちこちに書かれてしまう可能性があり、変更や拡張が煩雑になります。関連する処理がどこにあるのかが分かりにくくなり、修正漏れの発生や保守性が低下します。
ドメインモデル貧血症になる原因
データベース中心の設計
最初にDBのテーブルスキーマを決めて、それを元にコードを生成すると、モデルは「ただのデータ構造」になり、ロジックを持ちにくくなります。
サービスオブジェクトの誤用
サービスに全てのビジネスロジックを書くのが「設計ルール」と思い込み、値オブジェクトやエンティティを単なるDTOのように使ってしまうと、それらにドメイン知識が書かれず低凝集になります。
Fat Modelを避けようとして...
Fat Modelを避ける判断が、結果的にドメイン知識をモデルの外に分散させ、ドメインモデル貧血症につながる可能性があります。
既存プロダクトでドメイン駆動設計の導入途中の時
既存のプロダクトにおいて、ビジネスロジックがサービス層やコントローラー層に散らばっている場合、これをドメインモデルに移行するのは容易ではありません。移行が途中で止まったり、ロジックが分散したままの状態で、ドメインモデルが単なるデータ保持の役割を果たすことになりがちです。
Read系のAPIのリファクタでDDDを導入するときどうするか?
Read系APIでは、QueryServiceなど使うこともあります。
しかし、ドメインオブジェクトを使いたい時もあるでしょう。
この場合、DBから取得したデータをリポジトリでドメインオブジェクトに変換して返すこともあります。しかし、DBに保存されたデータそのものは信用せず、ドメイン知識をドメインオブジェクトに反映させることが重要です。
スケジュールに余裕がない場合、すぐにドメインオブジェクトにビジネスロジックやバリデーションを追加するのは難しいことがあります。この場合、最初はDBから取得したデータをそのまま利用することで短期的な手間を省けます。しかし、この方法を続けると、ドメインモデルが単なるデータ保持用オブジェクトとなり、DDDの目的であるドメイン知識やビジネスロジックのコード化が遅れます。
そのため、後でドメインオブジェクトにビジネスロジックを組み込む計画を立てることが大切です。最終的には、ドメインオブジェクトがビジネスロジックやルールを担うべき存在となり、ソフトウェアの凝集度や保守性を高めることになります。初期段階ではデータ構造として扱い、後でロジックを追加していくことで、ビジネスルールや振る舞いを反映させることが求められます。最初から完璧なドメインモデルを作る必要はないので、継続的にモデルを育てていきましょう。
最後に
ドメインモデル貧血症は、ドメイン駆動設計の本来の目的である「ドメイン知識をコードで表現すること」に反する設計状態です。単にデータを保持するだけのモデルでは、変更に強く、意図が明確なソフトウェアを実現することは難しいでしょう。よって、状況にもよりますが基本的にドメインモデル貧血症は悪いと言っても過言ではないと思います。
もちろん、すべてのモデルに一気にロジックを詰め込む必要はありません。特に既存プロダクトやリードタイムの厳しい環境では、段階的な導入が現実的です。まずは意識的に「ドメイン知識をどこに書くべきか?」を考え、少しずつでもモデルに責任を持たせていくことが、保守性の高い設計への第一歩となります。