前記事
4.不自然さを解決する「ドメインサービス」
- ソフトウェア開発の文脈で語られるサービスはクライアントのために何かを行うオブジェクト
- 大きく分けて2つ
- ドメインのためのサービス → ドメインサービス
- アプリケーションのためのサービス → アプリケーションサービス
- ドメインサービスとは
- 値オブジェクトやエンティティに記述すると不自然になってしまう振る舞いが存在する。
- ドメインサービスはそういった不自然さを解決するオブジェクト
- ユーザー名の重複を許さない → ドメインのルールはどこに書くべきか?
違和感がある重複チェック処理
var user = new User(userId, userName);
// エンティティに重複チェック処理があると違和感がある
var checkResult = user.Exists(user);
- 不自然さを解決するオブジェクト、ドメインサービス
- ドメインサービスは値オブジェクトやエンティティとは異なり、自身のふるまいを変更するようなインスタンス特有の状態を持たないオブジェクト
ドメインサービスを使用した重複チェック処理
var userService = new UserService();
var user = new User(userId, userName);
var checkResult = userService.Exists(user);
- 値オブジェクトやエンティティに記述すると不自然なふるまいはドメインサービスに記述する。ただし、「不自然なふるまいに」に限定すること
- ドメインオブジェクトに記述されるべき知識やふるまいが、ドメインサービスやアプリケーションサービスに記述されると、語るべきことを何も語っていないドメインオブジェクトの状態をドメインモデル貧血症という
- ドメインサービスか、値オブジェクトやエンティティか迷ったら、可能な限り値オブジェクトやエンティティに定義する。
- ロジックの点在はソフトウェアの変化を阻害する。コードを一元的に管理する。
- 物流拠点間で荷物を輸送する例。物流拠点に輸送メソッドを定義すると不自然なので、ドメインサービスとして定義した。
物流拠点間で荷物を輸送するドメインサービス
class TransportService {
// 輸送する
public void transport(PhysicalDistributionBase from, PhysicalDistributionBase to, Baggage baggage) {
// 荷物の出庫を行う
var shippedBaggage = from.ship(baggage);
// 荷物の入庫を行う
to.receive(shippedBaggage);
//輸送の記録を行う
}
}
- ドメインサービスの命名規則
- ドメインの概念
- ドメインの概念+Service
- ドメインの概念+DomainService
- XxxDomainService.XxxService でドメインサービスであることがわかる。
- ドメインと密接にかかわるサービスは、ドメインオブジェクト名+Serviceとする(UserServiceなど)
まとめ
- ドメインオブジェクトに実装すると不自然なふるまいが必ず存在する。
- 複数のドメインオブジェクトを横断するような操作に多く見られる。
- そんなときに活用するのがサービスと呼ばれるオブジェクト。
- ドメインモデル貧血症を起こさないためにも、ふるまいがどこに記述されるべきかということに細心の注意を払う。
5.データにまつわる処理を分散する「リポジトリ」
- リポジトリはデータを永続化し再構築するといった処理を抽象的に扱うためのオブジェクト
- データの永続化と再構築を直接行うのではなく、リポジトリを経由して行うというだけのことが、ソフトウェアに驚くほどの柔軟性を与える
- リポジトリはドメインオブジェクトではないが、技術的要素を引き受けるため、ドメインモデルを表現するドメインオブジェクトを際立たせる役目を持つ
-
Exits
メソッドなどユーザの重複確認はドメインのルールに近く、リポジトリに実装するのは責務として相応しくないので、ドメインサービスが主体となって行うべき。 - 見つからないときに
null
が返却されることに抵抗がある開発者はOption型を使う。 - リポジトリはインタフェースで定義して、実装する。
- 実装されたリポジトリは、コンストラクタでProgramクラスに引き渡す。
- データベースを使用したテストは、手間が積み重なると次第にやらなくなってくる。
- データベースを使用しないテストを行う。
- データベースを使用しないテスト用に実装したリポジトリを作成し、連想配列で疑似的なテーブルを作成し、連想配列に対して検索や保存できるようにする。 連想配列に影響ため、保存や返却するオブジェクトはディープコピーする。
- オブジェクトリレーショナルマッパー(ORM)を利用する手法がメジャー。
- 永続化に関するふるまい
Save
メソッドは、永続化を行うオブジェクトを引数にとる - ライフサイクルの破棄を行うメソッド
Delete
はリポジトリに定義される - 識別子によって検索されるメソッド
Find
- すべてのオブジェクトを再構築するメソッド
FindAll
- コンピュータのリソースを食いつぶす可能性がある
- 検索に適したメソッド
FindByUserName
検索に利用するデータを引数で受け取る
まとめ
- リポジトリを利用するとデータの永続化にまつわる処理を抽象化することができる
- 驚くほどの柔軟性をソフトウェアに与える
- リポジトリを実装して差し替えられる(インメモリ→高性能のデータストア)
- テスト用リポジトリでテストが実行できる
- ただし、実環境上でテストを行うことは不可欠
- リポジトリをうまく活用して処理の意図を明確にすることは、後続の開発の助けになる。
6.ユースケースを実現する「アプリケーションサービス」
- ユースケースを実現するオブジェクト。ドメインオブジェクトを組み合わせて実行するスクリプトのようなふるまい。
- ユーザー機能のユースケース
- ユーザを登録する
- ユーザ情報を確認する
- ユーザ情報を更新する
- 退会する
- ユーザIDを表す値オブジェクト
- UserId
- ユーザ名を表す値オブジェクト
- UserName -
- ユーザを表すエンティティ
- Create
- ChangeName
- ユーザのドメインサービス
- Exists
- ユーザのリポジトリ
- Find
- FindByName
- Save
- Delete
- ユーザのアプリケーションサービス(上記メソッドを並べる)
- Register(string name)
- Create(ユーザインスタンスを作成)
- Exists(すでにユーザ名が存在するか?)
- Save(リポジトリに保存)
- Get(string userId)
- UserId(引数からユーザIDを作成)
- Find(ユーザIDで検索してユーザエンティティ取得)
- Update(string userId, string name)
- UserId(引数からユーザIDを作成)
- Find(ユーザエンティティ取得)
- UserName(引数からユーザ名を作成)
- ChangeName(ユーザ名を変更)
- Exists(重複確認)
- Save(リポジトリに保存)
- Delete(string userId)
- UserId(引数からユーザIDを作成)
- Find(ユーザエンティティ取得 見つからなかったらExceptionとする or 成功とする場合もある)
- Delete(リポジトリから削除)
- Register(string name)
-
全く関係がないクラスが、直接ドメインオブジェクトを操作してはならない
- OK
- Userアプリケーションサービス→UserドメインサービスやUserエンティティ
- Clientアプリケーションサービス→UsetDto→Userアプリケーションサービス→UserドメインサービスやUserエンティティ
- NG
- Clientアプリケーションサービス→UserドメインサービスやUserエンティティ
- OK
- UserData(DTO)クラスにUserエンティティからのCreate(コンストラクタ)を作成しておくと変換が楽。
- ユーザ項目を更新する場合、更新する項目のみ値をセットするUserUpdateCommandクラスを作成する
- アプリケーションサービスにはドメインのルールは記述されるべきではない
- 凝縮度はモジュールの責任範囲がどれだけ集中しているかを測る尺度。
- 凝縮度を高めたほうが好ましい。堅牢性、信頼性、再利用性、可読性が上がる。
- LCOM(Lack of Cohesion in Methods) 凝縮度を測る単位。
- すべてのインスタンス変数はすべてのメソッドで使われるべき。
- インスタンス変数の使われ方に偏りがある場合、クラス分割を考える。
- まとまりを表現するために使われるのがパッケージ
- Application(パッケージ)
- Users(パッケージ)
- UserRegisterService(アプリケーションサービス)
- UserGetInfoService(アプリケーションサービス)
- UserUpdateInfoService(アプリケーションサービス)
- UserDeleteService(アプリケーションサービス)
- Users(パッケージ)
- アプリケーションサービスをインターフェース定義して実装することにより、テスト用の実装に差し替えることができる。
- サービスは、自身のためのふるまいを持たない。活動や行動であることが多い。
- ドメインサービスは、ドメインの知識を表現したオブジェクト。重複確認など。
- アプリケーションサービスは利用者の問題を解決するために作られる。
- ドメインサービスとアプリケーションサービスは対象となる領域が異なるだけで本質的には同じもの。
- サービスの向いている方向がドメインであるかアプリケーションであるかで分けられる。
- サービスは自身のふるまいを変化させる目的で状態を保持しない。
- 状態を一切持たないということではない。(リポジトリやドメインサービスのインスタンスは持つ)
まとめ
- アプリケーションサービスはドメインサービスのオブジェクトの操作に徹することでユースケースを実現する。
- アプリケーションサービスにドメインのルールを記述しない。ドメインのルールはドメインオブジェクトに実装する。
参考
ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本
www.amazon.co.jp/dp/B082WXZVPC