architectural patterns in building modular domain models

86
Architectural Patterns in Building Modular Domain Models Debasish Ghosh @debasishg

Upload: debasish-ghosh

Post on 16-Mar-2018

393 views

Category:

Software


3 download

TRANSCRIPT

Architectural Patterns in Building Modular

Domain ModelsDebasish Ghosh

@debasishg

Architectural Patterns in Building Modular

Domain ModelsDebasish Ghosh

@debasishg

Architectural Patterns in Building Modular

Domain ModelsDebasish Ghosh

@debasishg

Architectural Patterns in Building Modular

Domain ModelsDebasish Ghosh

@debasishg

— 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

Patterns of modularization of domain models

Algebraic patterns of modularization of domain models

Algebraic patterns of modularization of domain models

using pure values

Algebraic patterns of modularization of domain models

using pure values as effects

Algebraic patterns of modularization of domain models

using pure values as effects

even in the presence of side-effects

Domain Modeling

Domain Modeling(Functional)

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”

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

Your domain model is a function

Your domain model is a function

Your domain model is a collection of

functions

Your domain model is a collection of

functionssome simpler models are ..

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

• Functions / Morphisms

• Types / Sets

• Composition

• Rules / Laws algebra

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 ?

Problem Domain

Bank

Account

Trade

Customer

......

...

Problem Domain

...

entities

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

do trade

process execution

place order

Solution Domain

...

behaviorsFunctions

([Type] => Type)

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

1

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

modularity

side-effects don’t compose

.. the semantics of compositionality ..

in the presence of side-effects

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 ..

F[A]

The answer that the effect computesThe additional stuff

modeling the computation

• 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

Effects

Side-effects

- 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

Questions?