Dependent method types を利用した軽量Clean Architecture の紹介
+ - 0:00:00
Notes for current slide
Notes for next slide

Dependent method types
を利用した
軽量Clean Architecture
の紹介

Scala関西Summit 2019 10/26

1 / 27

自己紹介

  • 中村 学(Nakamura Manabu)
  • @gakuzzzz
  • Tech to Value 代表取締役
  • Opt Technologies 技術顧問 Opt Technologies
  • F-CODE CTO f-code
2 / 27

ドメインを表現するコードから
技術詳細を分離したい

3 / 27

ドメインを表現するコードから
技術詳細を分離したい

これを実現するためにScalaの持つ高度な抽象化能力を使った方法が提案されています。

  • Free Monadを利用したDSL
  • typed-final(tagless-final) を利用したDSL

以前のScala関西Summitや他のイベントでも様々な発表がありましたので、 興味のある方はぜひ調べてみて下さい。

4 / 27
  • Free Monadを利用したDSL
  • typed-final(tagless-final) を利用したDSL

これらの方法は確かに技術詳細をきれいに分離する事ができ、利用側での柔軟性も高めることができて大変有用です。

ただし、現在紹介されているような形ではモナド等の高度な抽象概念を利用するため、そういった概念を適切に理解していないとコード自体の理解も難しくなってしまうという状況があります。

5 / 27

ドメインを表現するコードから
技術詳細を分離したい

そこで、このセッションでは
Dependent method types を利用して、
モナド等の抽象化を使わずに技術詳細を分離する方法を紹介します。

6 / 27

Dependent method types

7 / 27

Dependent method types とは

簡単に言うと、ある引数の値に応じて他の引数や戻り値の型が変わるメソッドを定義することができる仕組みです。

例えば以下のbarメソッドは引数fooの値に応じて戻り値の型が変わります。

trait Foo {
type Result
def value: Result
}
def bar(foo: Foo): foo.Result = foo.value
8 / 27

Dependent method types とは

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 = piyopiyp
scala> bar(IntFoo)
res1: IntFoo.Result = 10
9 / 27

Dependent method types をどう使うのか

10 / 27

まず、ドメイン上の表現に実装詳細がべったり現れているコードから考えましょう。

package domain
// ドメイン上の表現になんか色々実装上の都合がまざってる
trait UserRepository {
def resolveByEmail(email: MailAddress)(
implicit ec: ExecutionContext, session: DBSession
): Future[Either[UserNotFound, User]]
}

この例ではドメイン上の表現であるUserRepositoryに実装の詳細であるExecutionContextFuture, DBSessionといった型が現れてしまっています。

11 / 27
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]
}
12 / 27
package domain
// ドメイン上の表現になんか色々実装上の都合がまざってる
trait UserRepository {
def resolveByEmail(email: MailAddress)(
implicit ec: ExecutionContext, session: DBSession
): Future[Either[UserNotFound, User]]
}

こういった仕様と実装を分離するための仕組みとして、僕らが古典的に使ってる「名前をつけてインターフェイスと実装クラスに分離する」という方法を思い出して下さい。

13 / 27

インターフェイスと実装クラスに分離する」という方法を考えると、ドメイン固有の名前をつけたtraitを定義して実装を隠蔽すればいいように思います。

package domain
trait MyCoolAppContext
trait 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 domain
trait UserRepository {
def resolveByEmail(email: MailAddress)(
implicit ctx: MyCoolAppContext
): MyCoolAppResult[UserNotFound, User]
}

こうすればMyCoolAppContextの実装クラスにDBSessionなどの実装詳細を分離することができます。

14 / 27
package domain
trait UserRepository {
def resolveByEmail(email: MailAddress)(
implicit ctx: MyCoolAppContext
): MyCoolAppResult[UserNotFound, User]
}

ただし上記のままでは MyCoolAppContextMyCoolAppResult の実装クラスが密接に関係を持つ事を表現できない事が問題になってきます。

例えば MyCoolAppResult の実装ではFutureを使いたいためExecutionContextが必要ですが、MyCoolAppContextの実装クラスがExecutionContextを保持してるとは限りません。

15 / 27

そこで Dependent method types を使って、この二つを結びつけてしまいます。

package domain
trait 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 domain
trait UserRepository {
def resolveByEmail(email: MailAddress)(
implicit ctx: MyCoolAppContext
): ctx.Result[UserNotFound, User] // ここで Dependent method types 使う
}
16 / 27

そうする事でinfra側では自由に実装クラスを定義することができます。

package infra
class 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
}
}
}
}
17 / 27

またドメイン上の表現からMyCoolAppResultインスタンスを作れるように、MyCoolAppContextに諸々便利メソッドを足しておきます。

package domain
trait 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 infra
class 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)))
}
18 / 27

複数のドメイン表現が同じMyCoolAppContextの実装を使うことを型上で表現できるようにするため、リポジトリなどに型引数を足していきます。

package domain
trait UserRepository[Context <: MyCoolAppContext] {
def resolveByEmail(email: MailAddress)(
implicit ctx: Context
): ctx.Result[UserNotFound, User]
}
19 / 27

そうすることで usecase などは完全にドメイン上の表現だけでコードを書く事ができます。

package usecase
class 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式で合成できる
}
}
20 / 27

こうなれば残るはinfra内でリポジトリ等の実装を定義してゆくだけとなります。

package infra
trait 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)
}
}
21 / 27

全ての要素が実装できたら後はアプリケーション層で単純にDIをしてやればおしまいです。

package application
import 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内に限定させることができました。

22 / 27

このようにドメイン上のコンテキストと戻り値の表現に専用のインターフェイスを用意し、Dependent method types を使ってその二つを関連付けることによって、ドメイン上の表現から実装詳細を分離することができます。

ここまで見てきたように、Scalaz や cats の Monad 等の型クラスがコード上に現れることもありません。

// 再掲
package domain
trait UserRepository {
def resolveByEmail(email: MailAddress)(
implicit ctx: MyCoolAppContext
): ctx.Result[UserNotFound, User] // ここで Dependent method types 使う
}
23 / 27

MyCoolAppContextImpl の実装は少し慣れてないと難しいですが、それ以外のコードについてはfor式が使えれば特に躓くポイントは少ないと思います。

従って MyCoolAppContextImpl の実装だけテックリードなどがガッと書いてしまえれば、Scalaにまだ不慣れなメンバーがいるチームでも採用しやすいアプローチかと思います。

24 / 27

まとめ

  • ドメインと実装詳細を分離する手法についてScalaの言語機構を活かした方法が様々提案されています。
  • Dependent method typesを使うとScalazやcatsなどの高度に抽象化された概念を使わなくても標準の言語機構だけで分離を行うことができます。
  • インターフェイスを定義して実装クラスを分離するという古典的アプローチは単純で扱いやすいです。
  • 今つくろうとしているシステムの要件に最適な実装手段を選択しましょう。
25 / 27

宣伝

Opt Technologies ではエンジニアを募集しています

エフ・コードではエンジニアを募集しています

26 / 27

質問とか

27 / 27

自己紹介

  • 中村 学(Nakamura Manabu)
  • @gakuzzzz
  • Tech to Value 代表取締役
  • Opt Technologies 技術顧問 Opt Technologies
  • F-CODE CTO f-code
2 / 27
Paused

Help

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