Scalastyleで特定のパッケージに依存できないようにする


ドメイン駆動設計では、あるBoundedContextが外部のBoundedContextに依存する場合、外部のドメインモデルに直接依存するのはアンチプラクティスです。ここでは、Scalaの静的解析ツールScalastyleを使ってContext同士がスパゲッティになることを予防する方法を紹介します。

BoundedContextの境界線をはっきりさせるために、各BoundedContextは別プロセスになるようなアーキテクチャ(例えば、Microservicesやサービス指向アーキテクチャ)にしたり、コードベースも全く別のリポジトリするのが理想です。さらに大規模なプロジェクトなら、開発チームもContextごとに組織が分かれていたほうがいいです。

しかし、小さなプロジェクトでは、教科書通りに分割してしまうと、Published LanguageとしてのREST APIの設計に余計な工数がかかったり、リポジトリがバラバラであるため、チケットの管理が大掛かりになったりします。(僕のいるShouldBeeチームではMicroservicesをアーキテクチャとして採用したが、開発にオーバーヘッドがありすぎて、Context毎にばらばらだったリポジトリも統合し、各ContextがRESTやRemote Actor等でやりとりするアーキテクチャも見直しに入っている)

2,3人の小さなチームでドメイン駆動設計をする場合は、すべてのBoundedContextをひとつのリポジトリでコードを管理することをお勧めします。さらに、初期の段階では、各BoundedContextも1つのプロセスで動くようにするだけで十分です。Context同士の関係がスパゲッティにならないよう注意してさえいれば、いずれスケールに迫られたときにでも、最小のコストで、Contextを独立のサービスに切り出せるからです。

複数のContextをひとつのリポジトリに入れて開発していく場合、最新の注意を払わなければならないのが、Context間の結合方法です。あるContextが別のContextのドメインモデルに直接依存するのは大変良くありません。

その代わり、クライアント側のContextが依存しても良いレイヤーを設けるようにします。よくあるドメイン駆動設計のアーキテクチャデザインパターンとしては、Layered Architectureがあります。その中のApplication Layerは、クライアント側のContextが依存するのにピッタリの場所です。Application Layerを提供するContextはクライアントにとって最適で、かつ最小限のAPIを実装できるからです。

Scalastyleで特定のパッケージに依存できないようにする

背景的な話が長くなってしまいました。。。

要するに何が言いたかったかというと、理論的にはApplication Layerにのみ依存するのがいいということです。が、しかし、開発チーム全員がこのことを常に意識下に置いて開発するのは、現実的ではありません。単に開発者が忘れっぽいというケースだけでなく、IDEが自動importした結果、知らずのうちに依存している場合もあります。

例えば、frontendコンテキストのコードのどこかにimport account.domain.Userと一行書いてしまうだけで、frontendがaccountコンテキストのドメインモデルに依存する状況が、いとも簡単に出来上がってしまうからです…。1

同じコードベースに含まれる以上、Context同士を物理的に離しておく方法は多くありませんが、Scalaの静的解析ツールScalastyleを使うことで、Contextのドメインモデルがスパゲッティに絡み合うことを予防することができます。

やり方は簡単です。scalastyle-config.xmlにカスタムルールとして下記を追加するだけです。

scalastyle-config.xml
 <!-- Custom rules -->
 <check level="error" class="org.scalastyle.file.RegexChecker" enabled="true">
   <parameters>
     <parameter name="regex"><![CDATA[myapp\.account\.domain]]>  </parameter>
   </parameters>
   <customMessage>Don't depend on other context domain model directly.</customMessage>
 </check>

org.scalastyle.file.RegexCheckerは、<parameter name="regex" />に設定した正規表現にマッチするテキストを見つけると警告してくれます。これを活用して、依存してほしくないパッケージ名をパターンに入れておきます。

更に、 Scalastyleでエラーがある場合、コンパイルを止めるように build.sbt に設定しておけば、間違ってドメインモデルがimportされたコードが、コードベースにチェックインすることも避けられるでしょう。

もちろん、上記の設定では次のような方法で依存するといった抜け道もあります。

import myapp._
import account._
import domain._

このようなコードも正規表現で排除することもできますが、通常はimport myapp.account.domainと書くのが普通なので、こうした例外的なコードはコードレビューで発見できればいいのかと思います。できれば、依存関係を静的解析してCIで発見したいところですが。


  1. もちろん、Domain Layerすべてのクラスにprivate [account]とつけることも出来る。