🐏

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

2022/05/13に公開

概要

アーキテクチャ

プロジェクト構成

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

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フレームワークを利用したコードへ移し替えるには、インフラ・ドメイン・アプリケーション層はモデルとしてそのまま移行し、プレゼンテーション層をコントローラーとして実装し直せば良い。

基本パターンのみを用いた実装例

ユーザーを登録する実装例を以下に記す。

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

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

以下は「ユーザー名」の値オブジェクト例である。

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)に移す変える。それによって、ドメインオブジェクトの振る舞いをプレゼンテーション層で呼び出されないようにしている。

DTO

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では複数のオブジェクトがまとめられ一つの意味をもったオブジェクトが構築されるが、そうしたオブジェクトのグループに維持されるべき不変条件が存在する。

※不変条件 = ある処理の間、その真理値が真のまま変化しない述語

名前変更やユーザー追加などの外部からの操作は、全て集約ルートを経由して行うことでルートが責任を持って集約内部の不変条件を保つ。

仕様

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

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

以下は、仕様の実装例である(不完全だが)。

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