この記事は、現在会社で行われている輪読会における、筆者の発表担当箇所の要旨をまとめたものです。
輪読会の対象書籍は下記の通りです。
成瀬允宣(2020)『ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本』
本記事では、上記書籍の中のChapter 6 「ユースケースを実現する『アプリケーションサービス』」を筆者がまとめた要旨と筆者の感想を記述します。
Chapter 6 「ユースケースを実現する『アプリケーションサービス』」要旨
6.1 アプリケーションサービスとは
- アプリケーションサービスを端的に表現すると、ユースケースを実現するオブジェクト
- e.g. ユーザ登録の必要なシステムにおいてユーザ機能を実現するには、下記のユースケースが必要
- ユーザを登録する
- ユーザ情報を変更する
- アプリケーションサービスには、これらのユースケースに従ったふるまいが定義される
アプリケーションサービスという名前
- アプリケーション=利用者の目的に応じたプログラム
- アプリケーションの目的=
- 利用者の必要を満たす
- 利用者の目的を達成する
- これらを達成するためには、ドメインオブジェクトの力を束ねて導く必要がある
- ドメインオブジェクトを操作し、利用者の目的を達成するよう導くのが「アプリケーション」サービス
6.2 ユースケースを組み立てる
- 例としてSNSのユーザ機能を取り扱う
- ユーザ機能を実現するユースケース
- ユーザを登録する
- ユーザ情報を確認する
- ユーザ情報を更新する
- 退会する
- これらはいわゆるCRUD (CREATE, READ, UPDATE, DELETE)処理
- i.e. システムを開発する上では基本的な処理
6.2.1 ドメインオブジェクトから準備する
- このアプリケーションサービスが取り扱うドメインオブジェクト:ユーザ
- 今回取り扱うユーザの概念はライフサイクルがあるモデル
- →エンティティとして実装(リスト 6.1)
-
User
には同一性を識別するためのUserId
が属性として定義されている - ユーザ情報としての属性はユーザ名のみが定義されている
- 現在の所はプリミティブな文字列型をラップしただけのオブジェクト(リスト 6.2)
- ユーザの重複がないことを確認するために、ドメインサービスを用意する(リスト 6.3)
- ユーザの永続化や再構築を行う必要があるために、(ドメインモデルを表現するドメインオブジェクトではないが)リポジトリも必要(リスト 6.4)
6.2.2 ユーザ登録処理を作成する
- ユーザ登録処理をアプリケーションサービスのふるまいとして実装(リスト 6.5)
-
Register
メソッドで最初にUser
オブジェクトを生成 - 重複チェックをドメインサービスである
UserService
に依頼 - ユーザが重複していなかったら
IUserRepository
にインスタンスの永続化を依頼
-
- これまでに取り扱ってきた
Program
クラスとほぼ同じコード- i.e.
Program
クラスはアプリケーションサービス
- i.e.
6.2.3 ユーザ取得情報を処理する
- ユーザ情報取得処理を
UserApplicationService
に追加する - ユーザ情報取得処理は結果を返却する必要がある
- →結果となるオブジェクトとしてドメインオブジェクトをそのまま返すか否かは重要な分岐点
- ドメインオブジェクトを戻り値として公開する選択をした場合(リスト 6.6)
- 実装コードは比較的シンプルになる
- しかしわずかな危険性をはらむ
- →アプリケーションサービス以外のオブジェクトが、ドメインオブジェクトを自由に操作できてしまう
- e.g. アプリケーションを利用するクライアントが本来呼べるべきではない
target.ChangeName
を呼べてしまう
- e.g. アプリケーションを利用するクライアントが本来呼べるべきではない
- ドメインオブジェクトのふるまいを呼び出すのはアプリケーションサービスの役割。
- →その枠組みを超えてドメインオブジェクトが呼び出されてしまうと、本来アプリケーションとして提供されるべきであったコードが各所に散りばめられてしまう
- 本書の著者がお薦めするのはドメインオブジェクトを直接公開しない方針
- その代わりにデータ転送用オブジェクト(DTO、Data Transfer Object)にデータを移し替えて返却する(リスト 6.8)
- DTOに対するデータの移し替え処理はアプリケーションサービスの処理上に記述される(リスト 6.9)
- 外部公開するパラメータが増えた場合の修正が面倒になるので、可能であれば修正箇所をまとめたい
- 修正箇所をまとめるための戦術として、DTOのコンストラクタで
User
のインスタンスを引数として受け取る方法が考えられる(リスト 6.11) - DTOはドメインオブジェクトを直接公開した場合に比べてパフォーマンスが劣化するが、その影響はほとんどの場合微々たるもの
- ドメインオブジェクトを公開するかDTOを用意するか、どちらを選択するかはプロジェクトのポリシーによる
- その選択がソフトウェアの未来を左右する可能性を秘めた決定であることを認識した上で決定を下すべき
煩わしさを減らすために
- ドメインオブジェクトを公開しないことを決めた場合、記述量が増えることを嫌う開発メンバーから理解を得られない可能性がある
- →ドメインオブジェクトを指定するとDTOとなるクラスコードを生成するツールを作るなど、煩わしさを減らす手段を用意するのがよい
6.2.4 ユーザ情報更新処理を作成する
- 更新処理で項目毎に別々のユースケースとするか、単一のユースケースで複数項目を同時更新できるようにするか
- →どちらでも正解になり得る
- 今回は複数項目を同時に更新できるユースケースをサンプルとする
- 情報変更にあたって、どのパラメータを変更したいかは引数にどのデータを渡すかによって制御する
- その場合、パラメータが追加されるとメソッドのシグネチャが変更されることになる
- それを避けるために、処理のファサード1としてコマンドオブジェクトを定義する戦略がある(リスト 6.17)
エラーかそれとも例外か
- 処理が失敗したときにエラーを返却するか例外を送出するかは一長一短
- エラーを返却する場合、結果オブジェクトを返却することになり、失敗をハンドリングするかはクライアント開発者の任意となる
- 例外を送出する場合、戻り値が返却されず、クライアント開発者にエラーハンドリングを強制することになる
- パフォーマンスがわずかに劣る場合がある
- 戻り値のエラータイプによって送出されるエラーを表現することができなくなる
6.2.5 退会処理を作成する
- リポジトリから対象となるインスタンスを復元し、そのインスタンスの削除をリポジトリに依頼する(リスト6.20)
- ここではユーザが見つからない場合は例外を送出しているが、ユーザが見つからない場合も退会成功として正常終了する判断もある(リスト 6.21)
6.3 ドメインのルールの流出
- アプリケーションサービスはあくまでドメインオブジェクトのタスク調整に徹するべき
- アプリケーションサービスにドメインのルールを記述すべきではない
- 同じようなコードを点在させることに繋がるため
- e.g. 「ユーザの重複を許さない」というルールをアプリケーションサービスに記述した場合
- ユーザ登録処理に重複を確認するコードが書かれる(リスト 6.22)
- ユーザ情報を変更する際にも、同じように重複を確認するコードが書かれる(リスト 6.23)
- ルールが変更された場合(e.g. メールアドレスの重複を許さない)
- コードの量が膨大になった場合に修正箇所を網羅しきれない
- →修正漏れが発生し、バグを引き起こすのは目に見えている
- ドメインオブジェクトにこのルールを記述し、アプリケーションサービスからはドメインオブジェクトを利用するようにする(リスト 6.27 6.28)
- ルールが変更された場合
- まずドメインサービスを修正する
- アプリケーションサービスの中で、そのルールが記述されたメソッドを利用している箇所を確認し、必要に応じて修正する
- ルールが変更された場合
- ルールをドメインオブジェクトに記述することは、同じルールが点在することを防ぎ、修正漏れを起因とするバグを防ぐ効果がある
6.4 アプリケーションサービスと凝集度
- プログラムの凝集度とは、モジュールの責任範囲がどれだけ集中しているかを測る尺度
- 凝集度を高めることで、モジュールが1つの事柄に集中することになり、堅牢性・信頼性・再利用性・可読性の観点から好ましいとされる
- 凝集度を測る方法として、LCOM (Lack of Cohesion in Methods)という計算式がある
- 端的に言うと「すべてのインスタンス変数は全てのメソッドで使われるべき」というもの
6.4.1 凝集度が低いアプリケーションサービス
- e.g. ユーザ登録処理とユーザ退会処理が記述されたアプリケーションサービスの
UserApplicationService
クラス-
userService
フィールド- 登録時には重複確認のために利用されている
- 退会時には利用されていない
- →凝集度の観点から好ましくない状態にある
-
- 凝集度を高めるために
- →クラスを分割するのが簡単な対処法
-
UserApplicationService
を- ユーザ登録処理クラス
UserRegisterService
- ユーザ退会処理クラス
UserDeleteService
に分割
- ユーザ登録処理クラス
- 登録処理と退会処理は「ユーザ」という概念で繋がってはいるものの、目的や処理内容は真逆
- 責務を厳格に分担するのであれば、クラスが分かれるのは当然
- とはいえ、ユーザに関連する処理を俯瞰できるようにすべき
- パッケージや名前空間、ディレクトリ構造でまとめる
- ここで述べられているのは「ユースケースごとにクラスは必ず分けるべきである」という主張ではない
- 凝集度は絶対の指標ではない
- コードを整頓する際のヒントとして、凝集度の視点を頭の片隅に入れておくべき
6.5 アプリケーションサービスのインターフェース
- インターフェースを用意することはクライアントの利便性を高める
- インターフェースが用意されていれば、クライアント開発者は実際の実装を待たなくともモックオブジェクトを利用して開発が可能
- アプリケーションサービスで例外処理が発生した場合のクライアントの処理を実装したい場合も、モックオブジェクトに例外を送出させることで容易にテスト可能
6.6 サービスとは何か
- サービス=クライアントのために何かを行うモノ
- 値オブジェクトやエンティティは自身のためのふるまいを持つ
- →サービスは自身のためのふるまいを持たない
- →サービスはものごとではなく、活動や行動であることが多い
- サービスはどのような領域にも存在する
- ドメインにおける活動=ドメインサービス
- アプリケーションとして成り立たせるためのサービス=アプリケーションサービス
- ドメインサービスとアプリケーションサービスは本質的には同じもの
- 対象となる領域・向いている方向が異なるだけ
6.6.1 サービスは状態を持たない
- サービスは自身のふるまいを変化させる目的で状態を保持しない
- ただし、状態を一切持たないわけではない
- サービスのふるまいを変化させる状態を持つと、開発者はインスタンスがどういった状態にあるかを気にしなければならない
- →複雑になり、開発者が混乱するので、状態を持たせる以外の方法を考えるべき
6.7 まとめ
- アプリケーションサービスはドメインオブジェクトの操作に徹することでユースケースを実現する
- アプリケーションを記述する際は、ドメインのルールが記述されないように注意する
- ドメインのルールはドメインオブジェクトに実装する
感想
本章においてはドメインオブジェクトとアプリケーションサービスという2つのレイヤの目的や役割の違いが、具体的なコードを元にわかりやすく述べられていました。
特に「6.3 ドメインのルールの流出」で述べられていた、「アプリケーションサービスにドメインのルールが記述されていると、同じようなコードが点在してしまい、ルールが変更された際の修正漏れが発生し、バグの温床になる」というのは自身の経験上も心当たりがあり、身につまされる思いで読みました。
コードを記述する際はドメインオブジェクトとアプリケーションサービスという2つのレイヤをそれぞれ意識して分ける必要があると感じました。また、設計レビューやコードレビューの際にもそういった観点、およびその他に本章で提示されていたプログラムの凝集度やインターフェース2、「サービスは状態を持たない」といった観点でレビューを行うことが必要であると感じました。
その際にレビュアー・レビュイーともに共通認識・共通言語としてこれらの概念・観点・用語を持っていると、開発チームとしてスムーズに品質の高いコードを書くことに繋がるのではないかな、と感じました。
一方、(本筋から脱線した些末な点ではありますが)「6.2.4 ユーザ情報更新処理を作成する」内で言及されていた「エラーかそれとも例外か」という問題に関しては、本章で述べられていた「エラーは処理失敗時のハンドリングを強制できない」「例外はパフォーマンスを低下させる可能性がある」「例外は戻り値のエラータイプによって送出されるエラーを表現することができなくなる」といった点は使用する言語やライブラリ・フレームワークによっても異なるでしょうし、一概にはメリット・デメリットを決定できないと感じました。
使用する言語やライブラリ・フレームワーク等において確立されたノウハウやデファクトスタンダードな方法を参考にしたり、「プログラムの実行を止めてまでも絶対に見過ごせない致命的な処理の失敗なのか」「多少問題があっても、プログラムを止めずに先に進めるべき軽微な処理の失敗なのか」といった影響度の観点等から決定されるべき問題であると感じました。
こうしたエラーハンドリング・例外処理の問題に関しては、それをテーマにした世の中に本が沢山出版されている、コーディング上の永遠の課題であると思います。
そうした本でもまた輪読会・勉強会をやりたいな、と感じました。