Last active
August 19, 2019 01:20
-
-
Save taeguk/6153695aad263777f4ba56ce202d2438 to your computer and use it in GitHub Desktop.
Purely Functional Business Logic In Scala (https://taeguk2.blogspot.com/2019/08/purely-functional-business-logic-in.html)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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