Created
August 13, 2017 17:39
-
-
Save yannick-cw/5d877b7e7fc1a1c1b36027622165fd0c to your computer and use it in GitHub Desktop.
Complete example for a DDD implementation using Kleisli and EitherT with Future
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
import cats.data.{EitherT, Kleisli} | |
import common.{Amount, Valid} | |
import scala.concurrent.ExecutionContext.Implicits.global | |
import cats.implicits._ | |
import scala.collection.mutable | |
import scala.concurrent.duration.Duration | |
import scala.concurrent.{Await, Future} | |
/** | |
* Domain Elements | |
*/ | |
// entity | |
sealed trait Account { | |
def id: String | |
def balance: Balance | |
} | |
final case class SavingsAccount(id: String, balance: Balance) extends Account | |
object Account { | |
def updateBalance(a: Account, amount: Amount): Either[String, Account] = | |
if (a.balance.amount + amount >= 0) a match { | |
case s: SavingsAccount => Right(s.copy(balance = Balance(s.balance.amount + amount))) | |
} else Left(s"insufficient funds Account has ${a.balance.amount}, wanted $amount") | |
} | |
// value objects | |
case class Balance(amount: Amount) | |
object common { | |
type Amount = BigDecimal | |
type Valid[A] = EitherT[Future, String, A] | |
} | |
/** ------------------------------------------------- | |
* Domain Services abstract | |
*/ | |
trait AccountService[Account, Amount, Balance] { | |
type AccountOperation[A] = Kleisli[Valid, AccountRepository, A] | |
def open(balance: Balance): AccountOperation[Account] | |
def debit(no: String, amount: Amount): AccountOperation[Account] | |
def credit(no: String, amount: Amount): AccountOperation[Account] | |
def balance(no: String): AccountOperation[Balance] | |
def transfer(from: String, to: String, amount: Amount): AccountOperation[(Account, Account)] = | |
for { | |
a <- debit(from, amount) | |
b <- credit(to, amount) | |
} yield (a, b) | |
} | |
trait InterestCalculation {} | |
trait TaxCalculation {} | |
/** | |
* Implementation | |
*/ | |
class AccountServiceInterpreter extends AccountService[Account, Amount, Balance] { | |
def open(balance: Balance): AccountOperation[Account] = | |
Kleisli(_.store(SavingsAccount("id", balance))) | |
def debit(no: String, amount: Amount): AccountOperation[Account] = | |
credit(no, -amount) | |
def credit(no: String, amount: Amount): AccountOperation[Account] = Kleisli { repo => | |
(for { | |
a <- repo.query(no) | |
debitedA <- EitherT.fromEither[Future](Account.updateBalance(a, amount)) | |
storedA <- repo.store(debitedA) | |
} yield storedA): Valid[Account] | |
} | |
def balance(no: String): AccountOperation[Balance] = | |
Kleisli(_.balance(no): Valid[Balance]) | |
} | |
object AccountService extends AccountServiceInterpreter | |
/** ------------------------------------------------- | |
* Repository | |
*/ | |
trait AccountRepository { | |
def query(id: String): Valid[Account] | |
def store(a: Account): Valid[Account] | |
def balance(id: String): Valid[Balance] = | |
query(id).map(_.balance) | |
} | |
/** | |
* In Memory Implementation | |
*/ | |
object InMemAccountRepo extends AccountRepository { | |
val mem: mutable.HashMap[String, Account] = mutable.HashMap[String, Account]() | |
def query(id: String): Valid[Account] = | |
EitherT( | |
Future.successful( | |
mem.get(id) match { | |
case None => Left("Could not find user") | |
case Some(a) => Right(a) | |
} | |
)) | |
def store(a: Account): Valid[Account] = | |
EitherT(Future.successful { | |
mem.update(a.id, a) | |
Right(a): Either[String, Account] | |
}) | |
} | |
/** ------------------------------------------------- | |
* Usage | |
*/ | |
object Run extends App { | |
import AccountService._ | |
def op: AccountOperation[Balance] = | |
for { | |
a <- open(Balance(BigDecimal(99))) | |
credited <- credit(a.id, BigDecimal(22)) | |
debited <- debit(credited.id, BigDecimal(100)) | |
b <- balance(debited.id) | |
} yield b | |
val newBalance: Valid[Balance] = op.run(InMemAccountRepo) | |
println(Await.result(newBalance.value, Duration.Inf)) | |
// Right(Balance(21)) | |
val failed = debit("id", BigDecimal(22)).run(InMemAccountRepo) | |
println(Await.result(failed.value, Duration.Inf)) | |
// Left(insufficient funds Account has 21, wanted -22) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment