architectural patterns in building modular domain models
TRANSCRIPT
— John Hughes in Why Functional Programming Matters https://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.pdf
“[..] modularity is the key to successful programming. Languages that aim to improve productivity must support modular programming well. But new scope rules and mechanisms for separate compilation are not enough — modularity means more than modules. Our ability to decompose a problem into parts depends directly on our ability to glue solutions together. To support modular programming, a language must provide good glue. [..] Using these glues one can modularize programs in new and useful ways[…]. Smaller and more general modules can be reused more widely, easing subsequent programming. This explains why functional programs are so much smaller and easier to write than conventional ones.”
Intent
• Domain Model
• Domain Model Algebra
• Algebraic Combinators
• Compositionality
• Algebra as the basis of modularization of domain models
• Algebraic effects to keep your domain model pure, modular and compositional
Algebraic patterns of modularization of domain models
using pure values as effects
even in the presence of side-effects
What is a domain model ?
A domain model in problem solving and software engineering is a conceptual model of all the topics related to a specific problem. It describes the various entities, their attributes, roles, and relationships, plus the constraints that govern the problem domain. It does not describe the solutions to the problem.
Wikipedia (http://en.wikipedia.org/wiki/Domain_model)
The Functional Lens ..
“domain API evolution through algebraic composition”
Building larger domain behaviours out of smaller ones
The Functional Lens ..
“domain API evolution through algebraic composition”
Use compositionof pure functions and types
https://msdn.microsoft.com/en-us/library/jj591560.aspx
A Bounded Context
• has a consistent vocabulary
• a set of domain behaviors modeled as functions on domain objects implemented as types
• each of the behaviors honor a set of business rules
• related behaviors grouped as modules
Domain Model = ∪(i) Bounded Context(i)
Bounded Context = { m[T1,T2,..] | T(i) ∈ Types }
Module = { f(x,y,..) | p(x,y) ∈ Domain Rules }
• domain function• on an object of types x, y, ..• composes with other functions• closed under composition
• business rules
Domain Model Algebra
explicit verifiable• types• type constraints• functions between types
• type constraints• more constraints if you have DT• algebraic property based testing
(algebra of types, functions & lawsof the solution domain model)
Domain Model Algebra
• Algebra as the glue for binding domain model artifacts
• Algebras evolve by composition
Reusable combinators
Build larger abstractions out of smaller ones using properties
of compositionality
Algebra of a Monoidtrait Semigroup[A] {
def combine(x: A, y: A): A
}
trait Monoid[A] extends Semigroup[A] {
def empty: A
} parametricity
Algebra of a Foldabletrait Foldable[F[_]] {
def foldleft[A,B](as: F[A], z: B, f: (B, A) => B): B
def foldMap[A,B](as: F[A], f: A => B)
(implicit m: Monoid[B]): B =
foldleft(as, m.zero,
(b: B, a: A) => m.combine(b, f(a)))
}
Algebraic Combinators
def mapReduce[F[_], A, B](as: F[A])(f: A => B)
(implicit fd: Foldable[F], m: Monoid[B]): B =
fd.foldMap(as)(f)
Built out of PURE algebra ONLY
Uses the algebras of Monoid and Foldable
Domain Behaviorsobject Payments extends .. {
def valuation(payments: List[Payment]): Money = {
implicit val m: Monoid[Money] = MoneyAddMonoid
mapReduce(payments)(creditAmount)
}
def maxPayment(payments: List[Payment]): Money = {
implicit val m: Monoid[Money] = MoneyOrderMonoid
mapReduce(payments)(creditAmount)
}
}
Algebras => Functional Patterns
• Bits of domain elements evolving from reusable generic algebras
• The algebras we reuse already exist - as a designer we provide the implementation of those algebras in the context of the domain model
• The algebras are the patterns, the implementations are instances of patterns in the context of our domain model
Domain Model = ∪(i) Bounded Context(i)
Bounded Context = { m[T1,T2,..] | T(i) ∈ Types }
Module = { f(x,y,..) | p(x,y) ∈ Domain Rules }
• domain function• on an object of types x, y• composes with other functions• closed under composition
• business rules
• domain function• on an object of type x, y, ..• composes with other functions• closed under composition
Domain Model = ∪(i) Bounded Context(i)
Bounded Context = { m[T1,T2,..] | T(i) ∈ Types }
Module = { f(x, y, .. ) | p(x) ∈ Domain Rules }
• business rules(algebra)
(algebra)
Given all the properties of algebra, can we consider algebraic composition to be the basis of designing, implementing and modularizing domain models ?
Bank
Account
Trade
Customer
......
...
do trade
process execution
place order
Problem Domain
...
entities
behaviors
Bank
Account
Trade
Customer
......
...
do trade
process execution
place order
Problem Domain
...
market regulations
tax laws
brokerage commission
rates
...
entities
behaviors
laws
Bank
Account
Trade
Customer
......
...
do trade
process execution
place order
Solution Domain
...
entities
behaviorsfunctions
([Type] => Type)
algebraic data type
Bank
Account
Trade
Customer
......
...
do trade
process execution
place order
Solution Domain
...
market regulations
tax laws
brokerage commission
rates
...
entities
behaviors
laws
functions([Type] => Type)
algebraic data type business rules / invariants
Bank
Account
Trade
Customer
......
...
do trade
process execution
place order
Solution Domain
...
market regulations
tax laws
brokerage commission
rates
...
entities
behaviors
laws
functions([Type] => Type)
algebraic data type business rules / invariants
Monoid
Monad
...reusable algebra
Bank
Account
Trade
Customer
......
...
do trade
process execution
place order
Solution Domain
...
market regulations
tax laws
brokerage commission
rates
...
entities
behaviors
laws
functions([Type] => Type)
algebraic data type business rules / invariants
Monoid
Monad
...
Domain Algebra
Client places order- flexible format
Transform to internal domainmodel entity and place for execution
1 2
Client places order- flexible format
Transform to internal domainmodel entity and place for execution
Trade & Allocate toclient accounts
1 2
3
def fromClientOrder: ClientOrder => Order
def execute(market: Market, brokerAccount: Account) : Order => List[Execution]
def allocate(accounts: List[Account]) : List[Execution] => List[Trade]
trait Trading {
}
trait TradeComponent extends Trading with Logging with Auditing
algebra of domain behaviors / functions
functions aggregate upwards into modules
modules aggregate into larger modules
Takeaways ..
• Publish as much domain behavior as you can through the algebra of your APIs. Since algebra is compositional, it’s easy to extend later by stacking abstractions on top of existing ones.
• Types are important but in domain modeling, names must come from the vocabulary of the domain - Ubiquitous Language
• Group related functions into modules. Modules are also compositional in Scala.
• Functions aggregate into modules, modules aggregate into components.
.. so we have a decent algebra of our module, the names reflect the appropriate artifacts from the domain (ubiquitous language), the types are well published and we are quite explicit in what the behaviors do ..
1. Compositionality - How do we compose the 3 behaviors that we published to generate trade in the market and allocate to client accounts ?
2. Side-effects - We need to compose them alongside all side-effects that form a core part of all non trivial domain model implementations
• Error handling ?
• throw / catch exceptions is not RT
• Partiality ?
• partial functions can report runtime exceptions if invoked with unhandled arguments (violates RT)
• Reading configuration information from environment ?
• may result in code repetition if not properly handled
• Logging ?
• side-effects
Side-effects
Side-effects
• Database writes
• Writing to a message queue
• Reading from stdin / files
• Interacting with any external resource
• Changing state in place
The solution is to abstract side-effects into data types that are pure values for which referential transparency holds and which can be composed with other pure functional abstractions
The solution is to abstract side-effects into data types that are pure values for which referential transparency holds and which can be composed with other pure functional abstractions
The solution is to abstract side-effects into data type-constructors that are pure values for which referential transparency holds and which can be composed with other pure functional abstractions
Effects
Option[A]
Either[A,B]
(partiality)
(disjunction)
List[A](non-determinism)
Reader[E,A](read from environment aka dependency Injection)
Writer[W,A](logging)
State[S,A](state management)
IO[A](external side-effects)
.. and there are many many more ..
• The F[_] that we saw is an opaque type - it has no denotation till we give it one
• The denotation that we give to F[_] depends on the semantics of compositionality that we would like to have for our domain model behaviors
def fromClientOrder: ClientOrder => F[Order]
def execute(market: Market, brokerAccount: Account) : Order => F[List[Execution]]
def allocate(accounts: List[Account]) : List[Execution] => F[List[Trade]]
trait Trading {
}
• We haven’t yet given any denotation to the effect type
• We haven’t yet committed to any concrete effect type
• .. we have intentionally kept the algebra open for interpretation ..
• .. there are use cases where you would like to have multiple interpreters for the same algebra ..
class TradingInterpreter[F[_]] (implicit me: MonadError[F, Throwable]) extends Trading {
def fromClientOrder: ClientOrder => F[Order] = makeOrder(_) match { case Left(dv) => me.raiseError(new Exception(dv.message)) case Right(o) => o.pure[F] }
def execute(market: Market, brokerAccount: Account) : Order => F[List[Execution]] = ...
def allocate(accounts: List[Account]) : List[Execution] => F[List[Trade]] = ... }
One Sample Interpreter
• .. one lesson in modularity - commit to a concrete implementation as late as possible in the design ..
• .. we have just indicated that we want a monadic effect - we haven’t committed to any concrete monad type even in the interpreter ..
The Program
def tradeGeneration[M[_]: Monad](T: Trading[M]) = for {
order <- T.fromClientOrder(cor) executions <- T.execute(m1, ba, order) trades <- T.allocate(List(ca1, ca2, ca3), executions)
} yield trades
The Program
def tradeGeneration[M[_]: Monad](T: Trading[M]) = for {
order <- T.fromClientOrder(cor) executions <- T.execute(m1, ba, order) trades <- T.allocate(List(ca1, ca2, ca3), executions)
} yield trades
depends on the algebra only
The Program
def tradeGeneration[M[_]: Monad](T: Trading[M]) = for {
order <- T.fromClientOrder(cor) executions <- T.execute(m1, ba, order) trades <- T.allocate(List(ca1, ca2, ca3), executions)
} yield trades
depends on the algebra only
the story of what needs to be done, NOT HOW
The Programimport cats.effect.IO
def tradeGeneration[M[_]: Monad](T: Trading[M]) = for {
order <- T.fromClientOrder(cor) executions <- T.execute(m1, ba, order) trades <- T.allocate(List(ca1, ca2, ca3), executions)
} yield trades
object TradingComponent extends TradingInterpreter[IO]
tradeGeneration(TradingComponent).unsafeRunSync
The Programimport monix.eval.Task
def tradeGeneration[M[_]: Monad](T: Trading[M]) = for {
order <- T.fromClientOrder(cor) executions <- T.execute(m1, ba, order) trades <- T.allocate(List(ca1, ca2, ca3), executions)
} yield trades
object TradingComponent extends TradingInterpreter[Task]
tradeGeneration(TradingComponent)
The Programdef tradeGenerationLoggable[M[_]: Monad] (T: Trading[M], L: Logging[M]) = for {
_ <- L.info("starting order processing") order <- T.fromClientOrder(cor) executions <- T.execute(m1, ba, order) trades <- T.allocate(List(ca1, ca2, ca3), executions) _ <- L.info("allocation done")
} yield trades
object TradingComponent extends TradingInterpreter[IO] object LoggingComponent extends LoggingInterpreter[IO]
tradeGenerationLoggable(TradingComponent, LoggingComponent).unsafeRunSync
Raise the level of abstraction
trait Trading[F[_]] {
def fromClientOrder : Kleisli[F, ClientOrder, Order]
def execute(market: Market, brokerAccount: Account) : Kleisli[F, Order, List[Execution]]
def allocate(accounts: List[Account]) : Kleisli[F, List[Execution], List[Trade]] }
The Program
def tradeGeneration[M[_]: Monad](T: Trading[M]) : Kleisli[M, ClientOrder, List[Trade]] = {
T.fromClientOrder andThen T.execute(m1, ba) andThen T.allocate(List(ca1, ca2, ca3)) }
object TradingComponent extends TradingInterpreter[IO] val tk = tradeGeneration(TradingComponent)
tk(cor).unsafeRunSync
- Rob Norris at scale.bythebay.io talk - 2017 (https://www.youtube.com/watch?v=po3wmq4S15A)
“Effects and side-effects are not the same thing. Effects are good, side-effects are bugs. Their lexical similarity is really unfortunate because people often conflate the two ideas”
Testing
• Testable
• We did not commit to a concrete type upfront - some virtues of being lazy with evaluation
• For monadic effects, easier testing with the Id monad - just use a different implementation for the same algebra
def tradeGenerationLoggable[M[_]: Monad] (T: Trading[M], L: Logging[M]) = for {
_ <- L.info("starting order processing") order <- T.fromClientOrder(cor) executions <- T.execute(m1, ba, order) trades <- T.allocate(List(ca1, ca2, ca3), executions) _ <- L.info("allocation done")
} yield trades
Takeaways
• Modularity in the presence of side-effects is a challenge
• Algebraic modeling is the key to address this
• Effects as algebras are pure values that can compose based on laws
• Determine the type of effect based on the semantics of compositionality of your domain behaviors
Takeaways
• Compose effects parametrically
• Honor the law of using the least powerful abstraction that works
• Be polymorphic (parametric) as early as you can, commit to concrete types as late as you can