はじめに
今回は、ドメイン駆動設計を勉強し始めたひとや、プロジェクトで活用し始めた人がつまずきやすいドメインサービスについてまとめます。
エンティティや値オブジェクトと比べてドメインサービスはイメージが湧きにくいかもしれません。
今回は、原著を確認しつつ、筆者なりのドメインサービスの扱い方をまとめます。
あくまで筆者なりの解釈なので、実装の一例として参考にしていただければと思います。
対象読者
- ドメイン駆動設計を勉強し始めた人
- ドメイン駆動設計を取り入れ、実装をし始めたが、実装で迷ってしまう人
ドメインサービスとは
エヴァンス本での定義
まずは原著であるエリック・エヴァンスのドメイン駆動設計から定義に関わる部分を引用します。
ドメインの重要な操作でありながら、エンティティにも値オブジェクトにも自然な落ち着き場所を見つけることのできないものがある。その中には、本質的に活動や行動であって、物事でないものもあるが、我々のモデリングパラダイムはオブジェクトなので、とにかくオブジェクトに当てはめてみよう。
Eric Evans. エリック・エヴァンスのドメイン駆動設計 (p.103). Kindle 版.
ドメインにおける重要なプロセスや変換処理が、エンティティや値オブジェクトの自然な責務ではない場合、その操作は、サービスとして宣言される独立したインタフェースとしてモデルに追加すること。モデルの言語を用いてインタフェースを定義し、操作名が必ずユビキタス言語の一部になるようにすること。サービスには状態を持たせないこと。
Eric Evans. エリック・エヴァンスのドメイン駆動設計 (p.105). Kindle 版.
ドメインサービスの重要なポイント
ドメインサービスとはドメインの重要な操作でありながら、単純にモデルにできない、つまりエンティティや値オブジェクトにも自然な落ち着き場所を見つけることのできないものを扱うためのオブジェクトです。こうしたドメインの重要な操作をエンティティや値オブジェクトの責務として押し付けてしまうと、モデルに基づくオブジェクトの定義を歪めるか、意味のない不自然なオブジェクトを追加することになる、とエヴァンスは述べています。
また、エヴァンスは、ドメインサービスのポイントをいくつか上げています。
- 状態を持たせない
- 実体よりも活動、つまり名詞よりも動詞にちなんで命名される
- 操作名はユビキタス言語に由来し、引数と結果はドメインオブジェクトであるべき
- サービスは節度を持って使用し、エンティティと値オブジェクトからすべての振る舞いを奪ってはならない
優れたサービスの特徴
- 操作がドメインの概念に関係しており、その概念がエンティティや値オブジェクトの自然な一部ではない。
- ドメインモデルの他の要素の観点からインタフェースが定義されている。
- 操作に状態がない。
Eric Evans. エリック・エヴァンスのドメイン駆動設計 (p.104). Kindle 版.
実務でのドメインサービス利用例
ここで、筆者が実務でドメインサービスを作成するに当たって頻出する例を紹介します。
例1: ユーザー名の重複チェック
ユーザーを新規登録する際に、そのユーザー名がすでに使われていないかチェックする処理はドメインサービスの利用例としてよく挙げられます。
なぜドメインサービスが適切なのかを考えてみましょう。エンティティの責務として考えてみると、これから作ろうとしているUserエンティティは、重複チェックに成功しないと作成されないため、チェック時点では作成されていません。また、Userエンティティは他の全ユーザーの情報を持っているわけではないです。
しかしながら、ドメイン上「ユーザー名は一意でなければならない」というルールがあり、このルールはアプリケーション層に実装してしまうと、ドメインの知識がドメイン層外に漏れてしまいます。
実装の内容としては、リポジトリパターンと組み合わせて実装することになります。UserServiceのようなドメインサービスに、isUserNameAvailable(userName: string): booleanのようなメソッドを用意し、その内部でUserRepositoryを使ってチェックを行うのが、責務分担として非常に綺麗に実装できます。
例2: 口座間の振込処理
続いての例は、口座間の振込処理です。
銀行の口座間での振込処理は、振込元口座と振込先口座の両方に関わる処理です。
少し、エンティティのことを想像してみましょう。
口座エンティティは、口座番号や残高などのデータを持ち、「お金を引き出す」、「お金を預け入れる」といった振る舞いを持つでしょう。 振込という処理を口座エンティティのメソッドとして実装しようとすると、不自然な点が出てきます。
振込は振込元口座と振込先口座どちらの責務でしょうか?振込元口座が振込先口座の残高を直接操作するのはカプセル化の観点からも責務が大きすぎます。口座は、自分自身の残高を管理するのが主な責務です。
言い換えると、振込処理は「振込元口座からお金を引く」処理と「振込先口座にお金を預ける」処理の2つの処理を整合性を保ちながら実行する必要があります。このようなビジネスルールは、ドメインサービスとして実装するのが適切です。
AccountServiceのようなドメインサービスに、transfer(fromAccountId: string, toAccountId: string, amount: number): voidのようなメソッドを用意し、その内部でAccountRepositoryを使って振込元口座と振込先口座の両方の残高を更新する処理を実装します。
Account以外のドメインオブジェクトが関わる場合は、単にTransferServiceのようなドメインサービスを作成しするのもいいと思います。
このように、ドメインサービスがオーケストレーション、つまり指揮者のような役割を果たしてエンティティのメソッドを呼び出してビジネスプロセスを実現します。
ドメインサービスを使うべきかどうかの判断基準
ここまで紹介したドメインサービスとしての性質を踏まえて、ドメインサービスを使うべきかどうかの判断基準をまとめます。
- そのロジックは、どのエンティティや値オブジェクトの責務にも自然に当てはまらないか?
- 複数のドメインオブジェクトが関わる処理か?
- ドメインにおける重要なビジネスルールや計算処理を表しているか?
- 状態を持たない操作か?
上記に当てはまる場合は、ドメインサービスとして実装することを検討します。 このとき、状態を持たないことの他に、メソッド名がユビキタス言語かつ動詞であること、引数と戻り値がドメインオブジェクトであることも確認します。
また、入出力や外部I/Oが関わるような場合はアプリケーションサービスとして実装することを検討します。
ドメインサービスの実装例
class UserService {
private UserRepository $userRepository;
public function __construct(private readonly UserRepository $userRepository) {}
public function isUserNameAvailable(string $userName): bool {
return !$this->userRepository->existsByUserName($userName);
}
}
ドメインサービスに関わる余談
今回の記事を書くにあたって、あらためてエヴァンス本を読み返しまして気づいた点をまとめます。
時には、サービスがモデルオブジェクトになりすまし、何らかの操作を行う以上の意味を持たないオブジェクトのように見えることがある。こういう「実行者」は結局のところ、「マネージャ」などで終わる名前を持つことになる。これらは独自の状態を持たず、実行する操作以外にはドメインにおいて意味を持たない。それでも、サービスという解決策により、この独特なふるまいは少なくとも落ち着き先を得て、実際のモデルオブジェクトを混乱させることがなくなる。
Eric Evans. エリック・エヴァンスのドメイン駆動設計 (pp.103-104). Kindle 版.
サービスの内容が薄くなってしまうことは特に単純なCRUDだとよくありますが、モデルに書いてしまうには不自然であることを解消できます。また、アプリケーションサービスは基本的に再利用しないため、ドメインサービスに切り出しておくことで再利用性が高まります。もちろんテストもしやすくなりますね。
サービスという名前は、他のオブジェクトとの関係性を強調している。エンティティや値オブジェクトとは異なり、純粋にクライアントに対して何が実行できるかという観点から定義されるのだ。
Eric Evans. エリック・エヴァンスのドメイン駆動設計 (p.104). Kindle 版.
サービス、という単語は特にWEBの文脈だと都合よく使われており、度々膨れ上がったサービス層を目にします。この名前は、ドメインオブジェクトとの関係性を強調し、クライアント目線で何が実行できるか、つまりは実行する内容を表現した動詞が命名に採用されるらしいです。
例えば、ある銀行のアプリケーションでは、口座残高が一定限度額を下回ると、顧客に電子メールを送信するかもしれない。この電子メールシステムはインタフェースによってカプセル化され、場合によっては、代わりの通知手段もそこに含まれるかもしれない。このインタフェースが、インフラストラクチャ層のサービスである。
Eric Evans. エリック・エヴァンスのドメイン駆動設計 (p.105). Kindle 版.
サービスはドメイン層以外にも存在します。上記の例はインフラ層のサービスです。
アプリケーションサービスとドメインサービスの違いについては微妙な境界線があると書かれているのが以下の文章です。
多くのドメインサービスやアプリケーションサービスは、エンティティと値オブジェクトで構成される集合体の上に構築され、ドメインに本来備わっている能力をまとめ上げて、実際に何らかの処理を行うスクリプトのようにふるまう。エンティティと値オブジェクトは、ドメイン層の持つ能力への便利なアクセスを提供するには、粒度が細かすぎることが多い。ここで、ドメイン層とアプリケーション層の間に引かれている微妙な境界線に行き当たる。例えば、銀行業アプリケーションが、我々が分析できるよう取引を変換してスプレッドシートファイルにエクスポートできるとすると、そのエクスポートはアプリケーションサービスである。銀行業ドメインにおいて「ファイル形式」には意味がなく、ビジネスルールも関係してはいない。
Eric Evans. エリック・エヴァンスのドメイン駆動設計 (pp.105-106). Kindle 版.
よくある落とし穴
- ドメインに関わる手続きだが、複数のオブジェクトを扱わないようなものをサービスに定義しすぎてしまう
- 「貧血モデル」や「ドメインモデル貧血症」と呼ばれ、エンティティや値オブジェクトがすかすかになってしまう。
- 対策としてはドメインオブジェクトに処理を回帰するようにする。
- インフラ依存の混入
- ドメインサービスはあくまでドメイン層であり、業務を表現するため、具体的なHTTPやDBのクライアントを直接注入しないようにする
- Repositoryなどを利用して処理を記載する
まとめ
今回は、ドメインサービスについて調査しました。性質を理解して、インフラ層やアプリケーション層と区別しつつ、ドメインサービスを利用することで、業務上の仕様をきれいに表現するイメージができたかと思います。
人によって解釈の揺れやすい部分なので、ぜひチーム内で認識をあわせる際の参考にして下さい。
