🐏

ドメイン駆動設計入門の読書メモ&サンプルコードをScalaで実装してみる

2022/05/13に公開約5,500字

概要

アーキテクチャ

プロジェクト構成

レイヤードアーキテクチャライクなものを採用。
※参考図書ではレイヤードアーキテクチャとして紹介された構成にほとんど従っているが、インフラ層→ドメインモデル層に依存するなど「上から下への依存」だけではない構成になっている。

https://github.com/r-tomiyama/scala-ddd-sample/blob/f1a12665abd76581c10da0cf5694575713e0ff81/build.sbt#L15-L41

依存関係がわかりやすくなるのでマルチプロジェクト化している(必須ではない)

参考図書では「全てを別のプロジェクトにする」or 「アプリケーションとドメインだけを同じプロジェクトにする(ドメインオブジェクトのメソッドを呼び出せるクライアントはアプリケーションサービスに限定したい)」が推奨されている。言語機能はパッケージ構成を左右するので、言語機能に合わせたやり方をすれば良い。
Scalaではプロジェクト間の依存が設定できるので、全て別プロジェクトにしてみた。

※2022/5/25追記※

後日、rootモジュールで抽象に実装を与えることで、インフラ層が各レイヤーの外側にいるオニオンアーキテクチャにしてみた。
その際に、どのファイルをどのレイヤーに置くかを変更していない。こっちの方がしっくりくる。

https://github.com/r-tomiyama/scala-ddd-sample/blob/57ce7feef1f613d82e6e4127388dd42687083b15/build.sbt#L15-L50

ドメイン層

集約の生成を担うファクトリやリポジトリのインターフェース、仕様もここに所属させる。
ファクトリなどはそれを利用するドメインモデルとまとめておいておくが、ドメインサービスはサービスをディレクトリに分ける。ドメインモデルのまとめ方と必ずしも一致するわけではないため。

※参考図書ではドメインモデル・ドメインサービスは一つのプロジェクトになっている。
またこのドメイン層は境界づけられたコンテキストごとにプロジェクトを用意する。(なのでここはプロジェクトを分けない方がよかったかも)

アプリケーション層

ドメイン層の住人取りまとめる、ユースケースを実現するための進行役。

インフラ層

他の層を支える技術基盤へのアクセスを提供する。ドメインのための永続化行うモジュールなどが含まれる。
技術基盤ごとにパッケージを分けている。

プレゼンテーション層

ユーザーインターフェースとアプリケーションを結び付ける層で、今回はCLIの入出力を責務としている。

ユースケースを呼び出しているだけなので、交換可能。例えばWebアプリ用のMVCフレームワークなどを利用する場合はインフラ・ドメイン・アプリケーション層をModelにして、プレゼンテーション層の代わりにコントローラーを実装する。

実装例(基本のパターンを利用)

ドメインモデル:値オブジェクト

メインモデルを実装したオブジェクトで、システム固有の値を表す。

https://github.com/r-tomiyama/scala-ddd-sample/blob/f1a12665abd76581c10da0cf5694575713e0ff81/domainModel/src/main/scala/domainModel/user/UserName.scala#L1-L10

必要な理由

  • プリミティブなものに比べて表現力を増す
  • コンストラクタ等にルールを定義することで、不正な値を存在させない
  • ルールを集約することでロジックがクライアントに散在されることを防ぐ
  • ドメインの様々なルールを定義することで、コードをドキュメント化する。

ドメインモデル:エンティティ

同じくドメインモデルを実装したオブジェクトで、同一性により識別される。エンティティかどうかは「ライフサイクル、連続性が存在するか」が判断基準となる。

https://github.com/r-tomiyama/scala-ddd-sample/blob/f1a12665abd76581c10da0cf5694575713e0ff81/domainModel/src/main/scala/domainModel/user/User.scala#L1-L19

ドメインサービス

ドメインオブジェクトとして表現するには違和感があるが、ドメインの活動であるものを表現するオブジェクト。

https://github.com/r-tomiyama/scala-ddd-sample/blob/f1a12665abd76581c10da0cf5694575713e0ff81/domainService/src/main/scala/domainService/UserService.scala#L5-L9

ドメインモデルを表現するだけでは、アプリケーションとして成立しない。アプリケーションにするための技術が必要。ドメインロジックをドメインオブジェクトから流出させないために、ドメイン知識を表現するパターンと同じくらい重要。
ドメインサービスはドメインモデルのコード上の表現であるため、ドメインモデルを表現することに徹する。またドメインオブジェクトに記述すべき振る舞いは全てドメインサービスに移し替えができてしまうが、不自然なもの以外はエンティティや値オブジェクト自身で表現すべき。

リポジトリ(実装を書いてないので、インターフェースのみ)

ドメインオブジェクトの永続化・再構築を行う。
特定のインフラストラクチャ技術の処理をロジックから分離・抽象化することで、ソフトウェアを柔軟にする。

https://github.com/r-tomiyama/scala-ddd-sample/blob/f1a12665abd76581c10da0cf5694575713e0ff81/domainModel/src/main/scala/domainModel/user/IUserRepository.scala#L5-L15

ORMを利用するためDAO(Data Access Object)クラスは用意するが、入出力の型はドメインオブジェクトのクラスにする。そうすることでDAOをクラスをインフラ層から流出させない。

ユースケース

ドメインオブジェクトを協調させて、「ユーザー登録する」などのユースケースを実現する。

https://github.com/r-tomiyama/scala-ddd-sample/blob/f1a12665abd76581c10da0cf5694575713e0ff81/application/src/main/scala/application/UserUsecase.scala#L16-L23

プレゼンテーション層に渡す際にはデータ転送用オブジェクト(DTO, Data Transfer Object)に移す変える。ドメインオブジェクトの振る舞いをプレゼンテーション層で呼び出されないようにする。

https://github.com/r-tomiyama/scala-ddd-sample/blob/3b4cdd4631b4c3e50f0cec278dd64fe879f0faaa/application/src/main/scala/application/dto/User.scala#L1-L8

実装例(ファクトリ・仕様など発展的なパターンを利用)

エンティティ

https://github.com/r-tomiyama/scala-ddd-sample/blob/c4d4ce89e9f1d3d4c7b63341ee9758f92d990cd2/domainModel/src/main/scala/domainModel/circle/Circle.scala#L7-L26

この例は集約の境界(不変条件を守る境界)でもある。
OOPでは複数のオブジェクトがまとめられ、一つの意味をもったオブジェクトが構築されるが、そうしたオブジェクトのグループに維持されるべき不変条件が存在する。
※不変条件 = ある処理の間、その真理値が真のまま変化しない述語

また外部からの操作は全て集約ルートを経由して行われる。ルートが責任を持って集約内部の不変条件を保つ。(例: changeName addUser

ファクトリ(オブジェクト)

複雑なオブジェクトの生成処理を、別に定義したオブジェクト。複雑な生成過程の処理はドメインオブジェクトの趣旨をぼやけさせるため。

https://github.com/r-tomiyama/scala-ddd-sample/blob/c4d4ce89e9f1d3d4c7b63341ee9758f92d990cd2/domainModel/src/main/scala/domainModel/circle/ICircleFactory.scala#L1-L11

インターフェースをドメインモデル層においておくことで、ファクトリの存在に気づけるようにする。

https://github.com/r-tomiyama/scala-ddd-sample/blob/3b4cdd4631b4c3e50f0cec278dd64fe879f0faaa/infrastructure/src/main/scala/infrastructure/rdb/CircleFactory.scala#L8-L15

インフラ層にシーケンスを利用して採番処理を行うファクトリを実装する。ファクトリによってインスタンスを生成するので、ドメイン層にインフラ依存の処理が入り込まない。

仕様

あるオブジェクトがある評価基準に達しているかを判定するオブジェクト。

複雑な評価手順はアプリケーションに記述されてしまいがちだが、ドメインの重要なルールである。だからと言ってオブジェクトの評価処理を安直にオブジェクトに実装すると、オブジェクトの趣旨はぼやける。そのため仕様オブジェクトに分ける。

https://github.com/r-tomiyama/scala-ddd-sample/blob/3b4cdd4631b4c3e50f0cec278dd64fe879f0faaa/domainModel/src/main/scala/domainModel/circle/CircleFullSpecification.scala#L5-L12

※仕様はドメインオブジェクトだから、その内部でリポジトリを使用することを避ける考え方もある。その場合はファーストクラスコレクションを利用するなどの選択肢がある。
※上記のような例はデータ量が多い場合にパフォーマンスが悪い。そのため、ドメインの防衛にこだわりすぎずに検索条件をリポジトリに直接記述することも検討すべき。書き込みにおいてはドメインオブジェクトやそれに関わるものを積極的に利用し、検索においてはある程度緩和する考え方がある。(Command-query separationなど)

ユースケース

https://github.com/r-tomiyama/scala-ddd-sample/blob/84205aae6a76ce5448139708434d7f4216aee6a3/application/src/main/scala/application/CircleUsecase.scala#L54-L71

Discussion

ログインするとコメントできます