3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

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

主に、以下の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的には、決定を遅らせたい内容だったりするので、そのあたりをトレードしていくのかなって感じです。

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

以上です。

3
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?