色々と参考にさせていただいて、調べて試してって試行錯誤して、ようやく、動くコードが出来た。
主に、以下のSlideや、GitHubを参照させていただきました。Aoyamaさん、ありがとうございます
私のほーで作成させていただいたコードは、 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的には、決定を遅らせたい内容だったりするので、そのあたりをトレードしていくのかなって感じです。
まだ試行錯誤途中ですが、ここまで分かった内容で、まとめておきます。
以上です。