Kengo's blog

Technical articles about original projects, JVM, Static Analysis and JavaScript.

「ドメイン駆動設計入門」付録のGradle向け解釈

ITエンジニア本大賞2021で紹介されていた「ドメイン駆動設計入門」(以下、本書と呼ぶ)が、DDDを学ぶ上でわかりやすかったです。一応他のDDD本も数冊読んではいたのですが、どうしてもユビキタス言語や境界づけられたコンテキストなど”場合による”ものが頻出してしまい、結局どうすればいいんだ……となって手が動きにくかったのです。よくわからない概念の上にさらにわからない概念を積み重ねるので、どんどん混乱する感覚がありました。

反面、本書では副題の通りボトムアップ形式を採っているため、実際にどう手を動かせば良いのか細かく確認できました。また不明点を最小化しながら進むだけでなく、その概念を導入することでどんな問題が解決されるのかが実例をもって明示されており、私のような独学派にはとてもありがたかったです。

さて本書ではC#を使っています。Javaとたいして変わらないとはいえ細かいところは違ってきますので、ここでは付録で紹介されたソリューション(プロジェクト)構築方法をGradleユーザ向けに解釈したものを紹介します。なおウェブアプリケーションフレームワークとしてspring-bootを利用するものとしますが、他のフレームワークでも大差ないはずです。

サブプロジェクトによる分割

Mavenではサブモジュール、Gradleではサブプロジェクトを使ってプロジェクトを分割します。本書ではプロジェクト分割の方針を2つ紹介していますが、ここでは簡単のためすべてのレイヤが固有サブプロジェクトを持つ構成を説明します。

domainサブプロジェクトには値オブジェクトやエンティティ、ドメインサービス、仕様やリポジトリインタフェースを配置します。ほぼPOJOで済むため、依存はspring-contextのようなアノテーションのみとなるでしょう。

ドメインオブジェクトの振る舞いの多くがここに配置されるため、Javadocやunit testを優先的に充実させる必要があるでしょう。また equals()hashCode()を手書きするとコードやテストカバレッジの見通しが悪くなることから、Immutablesのようなコードジェネレータによって対策することも考えられます。

infrastrutureサブプロジェクトはdomainサブプロジェクトに含まれるリポジトリインタフェースの実装を配置します。このサブプロジェクトはJDBCドライバのような依存先ミドルウェア固有の依存を持ちます。productionで利用する実装に加え、オンメモリで動作する実装も別途用意することで、unit testを書きやすくできます。

このサブプロジェクトではクラスをpublicにする必要はありません。Springが自動的に@Repositoryで修飾されたクラスを見つけてインスタンスを作るためです。他サブプロジェクトのunit testから呼び出す必要がある場合のみ、publicにすると良いでしょう。

applicationサブプロジェクトはdomainサブプロジェクトとinfrastructureサブプロジェクトの双方に依存します。アプリケーションサービスによるユースケースの記述をメインとするサブプロジェクトです。

本書の方針に従うならば、このサブプロジェクトに含まれるクラスやメソッドのAPIにはドメインオブジェクトを露出させないことが望ましいと言えます。このため他サブプロジェクトには api configuration ではなく implementation configuration を使って依存します。これによりドメインオブジェクトの露出をコンパイルエラーとして発見できる可能性が上がります。詳細は後述します。

presentationサブプロジェクトはapplicationサブプロジェクトにのみ依存します。後述するGradleの機能により、presentationサブプロジェクト内ではドメインオブジェクトやRepositoryの実装に触れない状態を担保できます。MockMvcなどを使うことでunit testが重くなり、またこのサブプロジェクトをビルドしている際はGradleのworkerが空きがちなので、maxParallelForksオプションでテストを並列実行することの恩恵が大きいでしょう。

apiとimplementationの使い分け

ここまでで2つ、詳細を後述すると述べたことがあります:

  1. presentationサブプロジェクトに対するドメインオブジェクトの露出をコンパイルエラーとして発見する方法
  2. presentationサブプロジェクト内ではドメインオブジェクトやRepositoryの実装に触れない状態を担保する方法

これらは表現こそ異なりますがひとつの課題を示しています。すなわち「ドメインオブジェクトをpresentationサブプロジェクトに露出させたくない」です。 もちろん依存先のpublicフィールドや戻り値がドメインオブジェクトでないことをArchUnitなどで確認しても良いですが、Gradleならこれらをコンパイルエラーとして発見する方法があります。

API and implementation separationがこの課題に対する解決になります。これはAPIとして外部に露出している依存と、そうではない内部利用している依存とを明確に分けて定義するものです。つまりサブプロジェクトが内部利用している依存は、そのサブプロジェクトの利用者に対して開示する必要はないという考えです。

これにより、applicationサブプロジェクトが依存しているdomainサブプロジェクトやinfrastructureサブプロジェクトを、presentationサブプロジェクトのコンパイル時クラスパスに含めずプロジェクトをビルドすることができます。 依存に色を付ける必要があるという意味で手間は増えますが、そもそもAPIとして露出している依存は減らすべき(JLBP-2)という話もありますので”依存がAPIとして露出しているのか?”を意識することは有用です。Gradle公式ドキュメントによるとコンパイルのパフォーマンス改善も期待できるケースがあるそうです。

例えば以上のサブプロジェクトの依存関係を図示すると、以下のようになります。

f:id:eller:20210131174527p:plain
com.savvasdalkitsis.module-dependency-graph によって生成

api configurationを使うのは1箇所だけです。infrastructureサブプロジェクトに含まれるクラスはdomainサブプロジェクトに含まれるリポジトリインタフェースを実装しており、またドメインオブジェクトをその引数や戻り値に使っているため、api configurationを使って依存します。その他はすべて implementation configurationが使えます。

補足: アセット生成を独立したサブプロジェクトの責務とする理由

前出の図に含まれているfrontendサブプロジェクトは、HTMLやJSなどのアセットを生成するためのものです。ドメイン駆動からは外れますが解説します。

このサブプロジェクトだけ若干特殊で、npmやyarnのプロジェクトをfrontend-gradle-pluginを通じてビルドする作りになります。ReactやVueの開発をGradleで無理やり実現するのではなくnpmあるいはyarnを呼び出す形を取ることで、JavaScript開発の知見を無理なく導入するとともにフロントエンド開発者にGradleやJavaの学習を強要する必要性を減らせます。

ちなみにビルド性能の向上も期待できます。これはfrontend-gradle-pluginのタスク、特にinstallFrontendタスクが重いことと、Gradleは異なるプロジェクトに属するタスクだけparallel buildできることから、説明できます。なおプロジェクトと同数以上のworkerがいないと性能が出ないので、--max-workersオプションを使うかorg.gradle.workers.maxプロパティを使って明示的にワーカー数を引き上げておくことが望ましいです。以下に手元の環境でhyperfineした結果を貼っておきます:

Benchmark #分割前: ./gradlew clean build
  Time (mean ± σ):     28.356 s ±  2.624 s    [User: 1.419 s, System: 0.145 s]
  Range (min … max):   25.914 s … 33.624 s    10 runs

Benchmark #分割後 (4 workers): ./gradlew clean build
  Time (mean ± σ):     27.161 s ±  3.987 s    [User: 1.388 s, System: 0.137 s]
  Range (min … max):   23.202 s … 33.721 s    10 runs

Benchmark #分割後 (5 workers): ./gradlew clean build --max-workers=5
  Time (mean ± σ):     24.442 s ±  2.916 s    [User: 1.321 s, System: 0.135 s]
  Range (min … max):   21.799 s … 31.388 s    10 runs

まとめ

ドメイン駆動設計入門」付録のGradle向け解釈を述べました。本書は私のような、ドメイン駆動を手を動かしつつ独学したい方におすすめします。

本投稿が本書の内容を実践する上でGradleユーザの参考になれば幸いです。またGitHubに私の書いたプロジェクトを置いてありますので、具体的にプロジェクト設定を見てみたい方は参照ください。