プロジェクト構成の意図と実装例を示します。
Presentation、Gateway、Application の間に Adapter を仲介させることでテストをしやすくする目的があります。
Adapter を仲介させることで各コンポーネントの差し替えが可能になるため、モックなどを作りやすくなります。 例を見ていきましょう。
Adapter プロジェクトには Gateway と Application のインターフェイス(トレイト)を実装します。
package adapter
trait HelloApplication {
def sayHello(): Unit
}
trait HelloGateway {
def output(str: String): Unit
}
Application プロジェクトには HelloApplication
トレイトを継承し、業務ロジックを実装した HelloApplicationImpl
を実装します。
業務ロジックの中では HelloGateway
の output
メソッドを呼び出して標準出力に文字列を出すように指示します。
package application
import adapter.HelloApplication
class HelloApplicationImpl(helloGateway: HelloGateway) extends HelloApplication {
override def sayHello(): Unit = {
stdoutGateway.output("hello")
}
}
Gateway プロジェクトには HelloGateway
を実装した HelloGatewayImpl
を実装します。
このクラスでは渡された文字列を Scala 標準の println
を使って標準出力に出力しています。
package gateway
import adapter.HelloGateway
class HelloGatewayImpl extends HelloGateway {
def output(str: String): Unit = {
println(str)
}
}
Presentation プロジェクトでは /hello
に GET
リクエストが来たら HelloApplication#sayHello
を実行するように実装します。
package presentation
import adapter.HelloApplication
class HelloPresentation(helloApplication: HelloApplication) {
val route =
get {
path("hello") {
helloApplication.sayHello()
complete("ok")
}
}
}
コンストラクタで HelloApplication などを引数に取っているところは DI コンテナによって HelloApplication
であれば HelloApplicationImpl
など、型に応じた適切なオブジェクトを自動的に渡してくれます。
(どのトレイトにどのクラスをバインドするかは設定が必要)
注目すべきは、それぞれの import
文です。全て Adapter プロジェクトのトレイトしか import
していません。つまり、Adapter プロジェクトにしか依存していないと言えます。
HelloApplicationImpl のテストのため、標準出力に "hello"
を出す代わりにファイルに "hello"
を書き出す HelloGateway
のモックを作りたくなったらどうすれば良いでしょうか?
output
で渡された内容をファイルに書き出す HelloFileGatewayImpl
を実装するだけです。
package gateway
import adapter.HelloGateway
import java.io.PrintWriter
class HelloFileGatewayImpl extends HelloGateway {
def output(str: String): Unit = {
val file = new PrintWriter("test.txt")
file.write(str)
file.close()
}
}
HelloGateway
に HelloFileGatewayImpl
をバインドするよう DI コンテナを設定すれば、HelloApplicationImpl
で使われる HelloGateway
の実体は HelloFileGatewayImpl
になります。
ReadModel プロジェクトは RDBMS に接続するために存在しているため、外部システムへのアクセスを統括する Gateway に置くのが自然ですが、あえて外しています。
それは以下の理由からです。
- RDBMS に行うクエリ種類の数だけ Adapter にメソッドを生やす必要があり手間がかかる
- トランザクションスコープを柔軟に制御するためには Slick の DBIO を Gateway から Presentation や Application まで返す必要がある (結局 Slick に依存してしまう)
- ローカル環境では Docker を使って RDBMS を簡単に構築できるので DB をモック化する必要がない