ドメインサービスの失敗例
次に、ドメインサービスを導入するときの注意点について紹介します。
ドメインサービスの多用/誤用
ドメインサービスの危険な点は、ユビキタス言語ではないビジネスロジックを大量に記述できてしまう点です。特にDDDに慣れていない場合、トランザクションスクリプトで処理を書いてしまう可能性があります。
この問題は、使う必要がない箇所でドメインサービスを利用してしまうため、エンティティや値オブジェクトが空っぽの「ドメインモデル貧血症(5章)」を引き起こす危険性があります。
ドメインサービスのミニレイヤ
似たアンチパターンとして、ドメインサービスの「ミニレイヤ」を作ってしまう失敗があります。検討することなく最初からドメインサービスのレイヤを作ってしまうと、このレイヤが肥大化する傾向にあります。それを避けるため、ドメインサービスのレイヤを作る必要がないコンテキストでは導入しないようにします。
アプリケーションサービス層での実装
別の失敗として、ドメインサービスをアプリケーションサービスで実装してしまう間違いもあります。アプリケーションサービス(14章)は、トランザクションやセキュリティといった、ドメインの外側の関心ごとの実装を行う場所です。
例えば、先ほど紹介したソースコードの「BusinessPriorityCalculator」の計算処理は、現在のドメインに特化しており、外部に流出させたくない情報です。アプリケーションサービスは利用者側であり、ドメイン内部の不要なロジックを知る必要はありません。このような観点でドメインサービスとアプリケーションサービスの区別を意識しておくといいでしょう。
ドメインサービス導入時のポイント
次に、ドメインサービス導入時のポイントについて紹介していきます。まず、これまで紹介してきたように、値オブジェクト/エンティティ/集約の構成でドメインモデルを表現できていれば、ドメインサービスがなくても問題ありません。
しかし、エンティティと値オブジェクト単体では計算できないビジネスロジックがある場合にドメインサービスの利用を検討します。ドメインモデルの詳細がクライアント側に漏れ出ないようにしつつ、複数のエンティティと値オブジェクトを組み合わせて抽出したり合計したりするビジネスルールを記述します。
リファクタリングによるドメインサービスの導入
IDDD本では先ほど紹介したBusinessPriorityTotalsメソッドを、Productクラスからドメインサービスへリファクタリングするシナリオを紹介しています。
リファクタリング前の状態では、Productクラス(集約)からBacklogItemクラス(集約)を取り出すことになり、集約の内部からリポジトリを呼ぶことになります。しかし、この呼び出し方式はできる避けたほうがよいといわれているため、ドメインサービスを導入しました。
このように、最初からドメインサービスを導入するのではなく、ドメインモデルの発展の過程でリファクタリングを行い、ドメインサービスが導入されるというシナリオがよいと思われます。
単一責任の原則(SRP原則)
SaaSOvationのサンプルでは、Productクラスが複数の役割を担っていたため、ドメインサービスを導入するリファクタリングを行いました。このリファクタリングを行うに至った考え方として「単一責任の原則」が存在しています。
単一責任の原則(SRP:Single Responsibility Principle)とは、ロバート・マーチン氏の書籍「アジャイルソフトウェア開発の奥義」にて解説されているオブジェクト指向の設計原則で「クラスを変更する理由は1つ以上存在してはならない」ことです。
1つのクラスが複数の役割を受け持っている場合、複数の役割がつながってしまい、片側の変更が逆側に影響を与えてしまいます。その結果、想定外の不具合が発生してしまいます(もし2つの役割が必ず同時に変更されるのであれば、分離する必要はありません)。
「クラスを単一の責務で設計する」という考え方はよく聞きますが、SRP原則では「仕様変更」に注目することで、より具体的にクラスを分割し、正しい名前をつけることを促しています。
なお、オブラブの単一責任の原則の紹介ページにも「クラスに変更が起こる理由は、1つであるべき」「良い抽象には良い名前がつく」といったポイントがわかりやすく解説されていますので、目を通してみるといいでしょう。