Posted at

Tagless Final StyleなRepositoryのHigher Kindが異なる場合のUse Caseの書き方

色々と参考にさせていただいて、調べて試してって試行錯誤して、ようやく、動くコードが出来た。

主に、以下のSlideや、GitHubを参照させていただきました。Aoyamaさん、ありがとうございます :bow:

私のほーで作成させていただいたコードは、 https://github.com/yoshiyoshifujii/scala-tagless-final-example です。


Repository

Tagless Final StyleなRepositoryは、色んなところで目に触れられると思うので、ここでは逆にさくっと記載しておきます。

trait UserRepository[F[_]] {

def store(user: User): F[UserId]
}

trait AccountRepository[F[_]] {
def store(account: Account): F[AccountId]
}

で、今回の困ったちゃんは、この、それぞれのRepositoryを、同じUse Caseで扱いたいが、それぞれ、RDBとRedisとか異なる永続化をするとかって場合に、F[_]が異なるため、困っちゃうよねって話。

それでも、やっぱ、Use Caseに詳細は持ち込みたくないよね。方針だけで書きたいよねってのに対策した。


Use Case

方針だけで書くUse Caseは、以下。

import cats._

import cats.implicits._

class AccountCreateUseCase[F[_]: Monad: UserRepository: AccountRepository] {

def execute(name: String, email: String): F[String] = {
for {
userId <- implicitly[UserRepository[F]].store(User.create(name))
accountId <- implicitly[AccountRepository[F]].store(Account.create(email))
} yield s"$userId-$accountId"
}

}

ここでは、UserやAccountの生成ロジックとか、そういったドメインのあたりとかは適当に省略しています。

ポイントとしては、

class AccountCreateUseCase[F[_]: Monad: UserRepository: AccountRepository] {

ってしたうえで、

implicitly[UserRepository[F]]

とする必要がある。

ここで、UserRepositoryとAccountRepositoryを型クラスとして扱うようにしている。

なのだが、ここは、実は、あまりよく分かっておらず、お詳しい方にぜひ御教授いただきたい。

とにかく、このように書く必要があり、さらに、traitではダメでclassで書く必要があるみたい。

ちょっとググった感じで、 https://blog.scalac.io/2017/04/19/typeclasses-in-scala.html を拝読していると、どうも、

class AccountCreateUseCase[F[_]: Monad](implicit ur: UserRepository, ac: AccountRepository)

って書くのと同じ感じっぽいですね。

あとは、

import cats.implicits._

をしておかないと、for式に展開できないので注意が必要。


Adapters

詳細なところ。

UserRepositoryやAccountRepositoryの実装にあたるのだが、ここで、それぞれの、F[_]が異なるってあたりを以下の表現としてみた。

type SpecialF[A] = ReaderT[Future, (DBConnection, RedisConnection), A]

単純にする場合ですと、

type UserF[A] = ReaderT[Future, DBConnection, A]

type AccountF[A] = ReaderT[Future, RedisConnection, A]

ってなるのですが、これをすると、F[_]が異なるので、これを、typeのところで、1つにがっちゃんこすることで、実装のタイミングで、必要なConnectionを選んで扱うという力技にしてみた。

UserRepositoryの実装だけ示すと、以下のような感じになる。

  implicit def userRepository(implicit ec: ExecutionContext): UserRepository[SpecialF] =

new UserRepository[SpecialF] {
override def store(user: User): SpecialF[UserId] =
ReaderT {
case (db, _) =>
Future(user.id)
}
}


Main

Mainは、

    implicit val ec: ExecutionContext = ExecutionContext.global

val useCase = new AccountCreateUseCase[SpecialF]
val result = useCase
.execute("name", "email@email.com")
.run((new DBConnection, new RedisConnection))
assert(Await.result(result, 1.second) === "UserId(1)-AccountId(2)")

みたいな感じになる。

runに、tupleで、それぞれのConnectionを渡す感じで、実装側で必要なのを取る。


まとめ

いったん、これで動いたが、まだ微妙なところがある。

RDBと、Redisを同じProjectに含めていたり、相互に参照している感じではあるので、そこを切り離したいとなるとできなさそうだったり。

Adaptersを実装するときに、どういったモノが必要になるか判断していくのに、Use Caseが複雑だとかなり巨大なTypeができそうだったり…。

とはいえ、そのあたりは、詳細であり、Clean Architecture的には、決定を遅らせたい内容だったりするので、そのあたりをトレードしていくのかなって感じです。

まだ試行錯誤途中ですが、ここまで分かった内容で、まとめておきます。

以上です。