Skip to content

Instantly share code, notes, and snippets.

@taeguk
Last active August 19, 2019 01:20
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save taeguk/6153695aad263777f4ba56ce202d2438 to your computer and use it in GitHub Desktop.
Save taeguk/6153695aad263777f4ba56ce202d2438 to your computer and use it in GitHub Desktop.
/*
scalaVersion := "2.12.8"
scalacOptions ++= Seq(
"-Xfatal-warnings",
"-Ypartial-unification"
)
libraryDependencies += "org.typelevel" %% "cats-core" % "1.0.0"
libraryDependencies += "org.typelevel" %% "cats-effect" % "1.3.1"
libraryDependencies += "org.typelevel" %% "cats-effect-laws" % "1.3.1" % "test"
*/
import scala.language.higherKinds
import cats._
import cats.data._
import cats.effect._
import cats.implicits._
// 도메인 레이어
// 핵심 도메인 엔티티/로직/룰들이 위치하고 purely functional 하게 작성됩니다.
object DomainLayer {
case class User(name: String, money: Int)
sealed trait Menu
case class Drink(name: String, alcoholicity: Double) extends Menu
case class Food(name: String) extends Menu
case class MenuOrder(menu: Menu, price: Int)
case class Pub(
name: String,
menuBoard: Map[String, MenuOrder], // key: name of menu
dartGamePrice: Int
)
// RWST 를 좀 더 편하게 사용하기 위한 용도입니다.
// 이렇게 제네릭 trait 으로 감싸지 않고 직접 `RWST[~~~~].xxx` 와 같이 사용하게 되면, 사용할 때마다 매번 제너릭 파라미터를
// 넣어줘야해서 코드가 상당히 지저분해지게 됩니다.
trait LogicHelper[F[_], E, L, S] {
def ask(implicit F: Applicative[F], L: Monoid[L]): RWST[F, E, L, S, E] =
RWST.ask
def tell(l: L)(implicit F: Applicative[F]): RWST[F, E, L, S, Unit] =
RWST.tell(l)
def get(implicit F: Applicative[F], L: Monoid[L]): RWST[F, E, L, S, S] =
RWST.get
def modify(f: S => S)(implicit F: Applicative[F], L: Monoid[L]): RWST[F, E, L, S, Unit] =
RWST.modify(f)
def pure[A](a: A)(implicit F: Applicative[F], L: Monoid[L]): RWST[F, E, L, S, A] =
RWST.pure(a)
def raiseError[A](t: Throwable)(implicit F: MonadError[F, Throwable], L: Monoid[L]): RWST[F, E, L, S, A] =
RWST.liftF(F.raiseError(t))
}
// 비지니스 로직이 표현력있고 간결하게 작성됩니다. 마치 요구사항 명세서를 읽는 것 같은 느낌을 주기도 합니다.
trait PubLogics[F[_]] {
type PubLogic[A] = RWST[F, Pub, Chain[String], User, A]
object PubLogicHelper extends LogicHelper[F, Pub, Chain[String], User]
import PubLogicHelper._
def playDartGame(implicit F: MonadError[F, Throwable]): PubLogic[Unit] =
for {
_ <- tell(Chain.one("Play dart game"))
dartGamePrice <- ask.map(_.dartGamePrice)
currentMoney <- get.map(_.money)
_ <-
if (currentMoney >= dartGamePrice)
modify { user => user.copy(money = user.money - dartGamePrice) }
else
raiseError(new Exception(s"Money is not enough to play dart game. Money: $currentMoney, dart game price: $dartGamePrice"))
} yield ()
def orderMenu(menuName: String)(implicit F: MonadError[F, Throwable]): PubLogic[Menu] =
for {
_ <- tell(Chain.one(s"Order the menu: $menuName"))
menuBoard <- ask.map(_.menuBoard)
menuOrder <-
menuBoard.get(menuName) match {
case Some(_menuOrder) => pure(_menuOrder)
case None => raiseError(new Exception(s"Unknown menu: $menuName"))
}
(menu, menuPrice) = (menuOrder.menu, menuOrder.price)
currentMoney <- get.map(_.money)
_ <-
if (currentMoney >= menuPrice)
modify { user => user.copy(money = user.money - menuPrice) }
else
raiseError(new Exception(s"Money is not enough to order. Money: $currentMoney, menu price: $menuPrice"))
} yield menu
// 실제 프로젝트에서는 "어떻게 놀 것인지" 를 파라미터로 받아서 그 것을 바탕으로 로직을 구성해야 하지만,
// 이 코드는 어차피 개념증명을 위한 것이므로 다음과 같이 하드코딩하였습니다.
def playInPub(implicit F: MonadError[F, Throwable]): PubLogic[Chain[Menu]] =
for {
nacho <- orderMenu("nacho")
beer <- orderMenu("beer")
_ <- playDartGame
} yield Chain(nacho, beer)
}
}
// 어플리케이션 레이어
// 실질적으로 요청을 수행하며 그 과정에서 외부 통신, DB 접근, 서버 상태 변경등의 side effect 를 일으키게 됩니다.
object ApplicationLayer {
import DomainLayer._
case object PubLogicsWithIO extends PubLogics[IO]
case class PlayInPubRequest(/* 생략 */)
def playInPub(request: PlayInPubRequest, user: User, pub: Pub): Unit =
// 실제 프로젝트에서는 `request` 를 바탕으로 `playInPub` 에 들어갈 파라미터를 구성해야 하지만,
// 여기에서는 그냥 코드를 간단히 하기 위해 이 과정을 생략하였습니다.
// 또한 여기에서는 편의를 위해 그냥 `user` 과 `pub` 을 파라미터로 받도록 하였지만,
// 실제 프로젝트에서는 DB 를 통해서 읽어오는 등의 형태가 될 것 입니다.
PubLogicsWithIO.playInPub
.run(pub, user)
.map { case (logs, updatedUser, orderedMenus) =>
// 로직이 성공적으로 실행된 경우 그 결과를 바탕으로 각종 side effect 를 수행하면 됩니다.
// 이처럼 실제 핵심 비지니스 로직은 모두 purely functional 하게 작성되게 되고,
// side effect 를 발생시키는 코드는 최소화되고 응집되게 됩니다.
println(s"Put the logs to logging system: $logs")
println(s"Save the updated user state to database: $updatedUser")
println(s"Send the ordered menus to client: $orderedMenus")
}
.handleError { cause =>
// 로직을 수행하다가 중간에 실패하더라도 프로그램의 상태가 변하지 않습니다.
// 따라서 transaction 처리가 매우 용이합니다.
println(s"Failed to perform a logic: $cause")
}
.unsafeRunSync()
}
object BlogPosting extends App {
import ApplicationLayer._
import DomainLayer._
val cheapPub = Pub(
name = "Cheap Pub",
menuBoard = Map(
"nacho" -> MenuOrder(
menu = Food("nacho"),
price = 4000
),
"beer" -> MenuOrder(
menu = Drink("beer", 5.1),
price = 3500
)
),
dartGamePrice = 2000
)
val premiumPub = Pub(
name = "Premium Pub",
menuBoard = Map(
"nacho" -> MenuOrder(
menu = Food("nacho"),
price = 13000
),
"beer" -> MenuOrder(
menu = Drink("beer", 5.1),
price = 10000
)
),
dartGamePrice = 4000
)
val user = User(name = "taeguk", money = 25000)
println("-------------------------------------------")
playInPub(PlayInPubRequest(), user, cheapPub)
println("-------------------------------------------")
playInPub(PlayInPubRequest(), user, premiumPub)
println("-------------------------------------------")
/* 실행결과는 다음과 같습니다.
-------------------------------------------
Put the logs to logging system: Chain(Order the menu: nacho, Order the menu: beer, Play dart game)
Save the updated user state to database: User(taeguk,15500)
Send the ordered menus to client: Chain(Food(nacho), Drink(beer,5.1))
-------------------------------------------
Failed to perform a logic: java.lang.Exception: Money is not enough to play dart game. Money: 2000, dart game price: 4000
-------------------------------------------
*/
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment