Scala関西Summit 2019 10/26
これを実現するためにScalaの持つ高度な抽象化能力を使った方法が提案されています。
以前のScala関西Summitや他のイベントでも様々な発表がありましたので、 興味のある方はぜひ調べてみて下さい。
これらの方法は確かに技術詳細をきれいに分離する事ができ、利用側での柔軟性も高めることができて大変有用です。
ただし、現在紹介されているような形ではモナド等の高度な抽象概念を利用するため、そういった概念を適切に理解していないとコード自体の理解も難しくなってしまうという状況があります。
そこで、このセッションでは
Dependent method types を利用して、
モナド等の抽象化を使わずに技術詳細を分離する方法を紹介します。
簡単に言うと、ある引数の値に応じて他の引数や戻り値の型が変わるメソッドを定義することができる仕組みです。
例えば以下のbar
メソッドは引数foo
の値に応じて戻り値の型が変わります。
trait Foo { type Result def value: Result}
def bar(foo: Foo): foo.Result = foo.value
trait Foo { type Result def value: Result}
def bar(foo: Foo): foo.Result = foo.value
object StringFoo extends Foo { type Result = String val value = "piyopiyp"}object IntFoo extends Foo { type Result = Int val value = 10}
// 渡す値によって戻り値の型が変わるscala> bar(StringFoo)res0: StringFoo.Result = piyopiypscala> bar(IntFoo)res1: IntFoo.Result = 10
まず、ドメイン上の表現に実装詳細がべったり現れているコードから考えましょう。
package domain// ドメイン上の表現になんか色々実装上の都合がまざってるtrait UserRepository { def resolveByEmail(email: MailAddress)( implicit ec: ExecutionContext, session: DBSession ): Future[Either[UserNotFound, User]]}
この例ではドメイン上の表現であるUserRepository
に実装の詳細であるExecutionContext
やFuture
, DBSession
といった型が現れてしまっています。
package domain// ドメイン上の表現になんか色々実装上の都合がまざってるtrait UserRepository { def resolveByEmail(email: MailAddress)( implicit ec: ExecutionContext, session: DBSession ): Future[Either[UserNotFound, User]]}
typed-final を使う方法ではここに高階型引数を導入してこれらの依存を切り離そうとしていました。
package domain// F[_] で抽象化trait UserRepository[F[_]] { def resolveByEmail(email: MailAddress): F[User]}
package domain// ドメイン上の表現になんか色々実装上の都合がまざってるtrait UserRepository { def resolveByEmail(email: MailAddress)( implicit ec: ExecutionContext, session: DBSession ): Future[Either[UserNotFound, User]]}
こういった仕様と実装を分離するための仕組みとして、僕らが古典的に使ってる「名前をつけてインターフェイスと実装クラスに分離する」という方法を思い出して下さい。
「インターフェイスと実装クラスに分離する」という方法を考えると、ドメイン固有の名前をつけたtraitを定義して実装を隠蔽すればいいように思います。
package domaintrait MyCoolAppContexttrait MyCoolAppResult[+E, +A] { def map[B](f: A => B): MyCoolAppResult[E, B] def flatMap[B, EE >: E] (f: A => MyCoolAppResult[EE, B]): MyCoolAppResult[EE, B]}
package domaintrait UserRepository { def resolveByEmail(email: MailAddress)( implicit ctx: MyCoolAppContext ): MyCoolAppResult[UserNotFound, User]}
こうすればMyCoolAppContext
の実装クラスにDBSession
などの実装詳細を分離することができます。
package domaintrait UserRepository { def resolveByEmail(email: MailAddress)( implicit ctx: MyCoolAppContext ): MyCoolAppResult[UserNotFound, User]}
ただし上記のままでは MyCoolAppContext
と MyCoolAppResult
の実装クラスが密接に関係を持つ事を表現できない事が問題になってきます。
例えば MyCoolAppResult
の実装ではFuture
を使いたいためExecutionContext
が必要ですが、MyCoolAppContext
の実装クラスがExecutionContext
を保持してるとは限りません。
そこで Dependent method types を使って、この二つを結びつけてしまいます。
package domaintrait MyCoolAppContext { type Result[+E, +A] <: MyCoolAppResult[Result, E, A]}trait MyCoolAppResult[F[+_, +_], +E, +A] { def map[B](f: A => B): F[E, B] def flatMap[B, EE >: E](f: A => F[EE, B]): F[EE, B]}
package domaintrait UserRepository { def resolveByEmail(email: MailAddress)( implicit ctx: MyCoolAppContext ): ctx.Result[UserNotFound, User] // ここで Dependent method types 使う}
そうする事でinfra側では自由に実装クラスを定義することができます。
package infraclass MyCoolAppContextImpl(ec: ExecutionContext, dbSession: DBSession) extends domain.MyCoolAppContext { type Result[+E, +A] = MyCoolAppResultImpl[E, A] private implicit val _ec: ExecutionContext = ec case class MyCoolAppResultImpl[+E, +A](value: Future[Either[E, A]]) extends domain.MyCoolAppResult[MyCoolAppResultImpl, E, A] { def map[B](f: A => B): MyCoolAppResultImpl[E, B] = MyCoolAppResultImpl(value.map(_.map(f))) def flatMap[B, EE >: E]( f: A => MyCoolAppResultImpl[EE, B] ): MyCoolAppResultImpl[EE, B] = MyCoolAppResultImpl { value.flatMap { case Left(e) => Future.successful(Left(e)) case Right(a) => f(a).value } } }}
またドメイン上の表現からMyCoolAppResult
インスタンスを作れるように、MyCoolAppContext
に諸々便利メソッドを足しておきます。
package domaintrait MyCoolAppContext { type Result[+E, +A] <: MyCoolAppResult[Result, E, A] def success[A](a: A): Result[Nothing, A] def fail[E](e: E): Result[E, Nothing]}
package infraclass MyCoolAppContextImpl(ec: ExecutionContext, dbSession: DBSession) extends domain.MyCoolAppContext { type Result[+E, +A] = MyCoolAppResultImpl[E, A] ... def success[A](a: A): Result[Nothing, A] = MyCoolAppResultImpl(Future.successful(Right(a))) def fail[E](e: E): Result[E, Nothing] = MyCoolAppResultImpl(Future.successful(Left(e)))}
複数のドメイン表現が同じMyCoolAppContext
の実装を使うことを型上で表現できるようにするため、リポジトリなどに型引数を足していきます。
package domaintrait UserRepository[Context <: MyCoolAppContext] { def resolveByEmail(email: MailAddress)( implicit ctx: Context ): ctx.Result[UserNotFound, User]}
そうすることで usecase などは完全にドメイン上の表現だけでコードを書く事ができます。
package usecaseclass LogsInConsole[Context <: domain.MyCoolAppContext]( repository: domain.UserRepository[Context], auditService: domain.AuditService[Context],) { def run(email: MailAddress, plainPassword: PlainPassword)( implicit ctx: Context ): ctx.Result[AuthenticationError, User] = { import AuthenticationError._ for { user <- repository.resolveByEmail(email) isValidPass = user.hashedPassword.verify(plainPassword) _ <- ctx.unless(isValidPass)(VerificationFailed) _ <- auditService.recordLogin(user) } yield user // MyCoolAppResultにmapやflatMapを定義しておいたのでfor式で合成できる }}
こうなれば残るはinfra内でリポジトリ等の実装を定義してゆくだけとなります。
package infratrait UserRepositoryImpl extends domain.UserRepository[MyCoolAppContextImpl] { def resolveByEmail(email: MailAddress)( implicit ctx: MyCoolAppContextImpl ): ctx.Result[UserNotFound, User] = { implicit dbSession: DBSession = ctx.dbSession val userOpt: Option[User] = withSQL { select.from(User as u).where.eq(u.email, email) }.map(User(u)).single.apply() userOpt.fold(ctx.fail(UserNotFound))(ctx.success) }}
全ての要素が実装できたら後はアプリケーション層で単純にDIをしてやればおしまいです。
package applicationimport com.softwaremill.macwire.wire// DI用ライブラリ Macwire を使った例trait Components { type Context = MyCoolAppContextImpl lazy val loginUsecase: usecase.LogsInConsole[Context] = wire[usecase.LogsInConsole] lazy val userRepo: domain.UserRepository[Context] = wire[infra.UserRepositoryImpl] lazy val auditService: domain.AuditService[Context] = wire[infra.AuditServiceImpl]}
これで実装の都合を完全にドメイン上の表現から排除してinfra内に限定させることができました。
このようにドメイン上のコンテキストと戻り値の表現に専用のインターフェイスを用意し、Dependent method types を使ってその二つを関連付けることによって、ドメイン上の表現から実装詳細を分離することができます。
ここまで見てきたように、Scalaz や cats の Monad
等の型クラスがコード上に現れることもありません。
// 再掲package domaintrait UserRepository { def resolveByEmail(email: MailAddress)( implicit ctx: MyCoolAppContext ): ctx.Result[UserNotFound, User] // ここで Dependent method types 使う}
MyCoolAppContextImpl
の実装は少し慣れてないと難しいですが、それ以外のコードについてはfor式が使えれば特に躓くポイントは少ないと思います。
従って MyCoolAppContextImpl
の実装だけテックリードなどがガッと書いてしまえれば、Scalaにまだ不慣れなメンバーがいるチームでも採用しやすいアプローチかと思います。
Keyboard shortcuts
↑, ←, Pg Up, k | Go to previous slide |
↓, →, Pg Dn, Space, j | Go to next slide |
Home | Go to first slide |
End | Go to last slide |
Number + Return | Go to specific slide |
b / m / f | Toggle blackout / mirrored / fullscreen mode |
c | Clone slideshow |
p | Toggle presenter mode |
t | Restart the presentation timer |
?, h | Toggle this help |
Esc | Back to slideshow |