Behind OOD // domain modelling in a post-OO world
Ruslan Shevchenko Lynx Capital Partners
https://github.com/rssh @rssh1
Domain modelling
• Representation of business domain objects: • in ‘head’ of people • in code
Domain modelling.
• Outline typical OOD issues
• Build
• simple domain model for toy billing system. // scala, can be mapped to java.
• internal DSL
Domain modelling
• Traditional OO way: have layer of classes, which corresponds to domain objects.
• Extensibility via inheritance in some consistent Universal Ontology
• Intensional Equality [identity != attribute]
• Object instance <=> Entity in real world
Traditional OO WAY
• Human is an upright, featherless biped with broad, flat nails.
Traditional OO WAY
• Human is an upright, featherless biped with broad, flat nails.
Open for extensions close for modifications
Traditional OO WAY
• Human is an upright, featherless biped with broad, flat nails.
Open for extensions close for modifications
Traditional OO WAY
• Intensional Equality [ mutability ]
// same identity thought lifecycle
Traditional OO WAY
• Object in code <=> Object in real world
class Person { int getId(); String getName(); int getAge();
Set<Person> getChilds() }
— thread-safe ?
— persistent updates ?
Domain modelling
• Traditional OO way: have layer of classes, which corresponds to domain objects.
• Extensibility via inheritance in some consistent Universal Ontology
• Intensional Equality [identity != attribute]
• Object instance <=> Entity in real world
Domain modelling
• Traditional OO way: similar to early philosophy
• Very general
• Idealistic
• Fit to concrete cases may be undefined
Domain modelling
• Post OO way: describe limited set of objects and relationships.
• Algebra instead Ontology
• Existential equality [identity == same attributes]
• Rules of algebra <=> rules of reality.
Domain modelling: where to start.
• Example: description of small billing system
Subscriber
Billing System
Service PaymentPlanuse in accordance with
Service is Internet | Telephony
PaymentPlan is Monthly Fee for Quantity Limit and
Overhead cost per UnitUnit Internet is Gb in
Telephony is Minute and Bandwidth
TariffPlan: Use limit and quantity from service.
sealed trait Service { type Limits def quantity(l:Limits): BigDecimal }
case object Internet extends Service { case class Limits(gb:Int,bandwidth:Int) def quantity(l:Limits) = l.gb }
case object Telephony extends Service { type Limits = Int def quantity(l) = l
TariffPlan: Price per limit and charge
case class TariffPlan[Limits]( monthlyFee: BigDecimal, monthlyLimits: Limits, excessCharge: BigDecimal )
Subscriber ? Service ?
case class Subscriber( id, name, … )
trait BillingService { def checkServiceAccess(u:Subscriber,s:Service):Boolean
def rateServiceUsage(u:Subscriber,r:ServiceUsage):Subscriber
def ratePeriod(u:Subscriber, date: DateTime)
def acceptPayment(u:Subscriber, payment: Payment)
}
// Aggregate// let’s add to Subscriber all fields, what needed.
adding fields for Subscriber aggregates.
case class Subscriber(id, name, serviceInfos:Map[Service,SubscriberServiceInfo], account: BigDecimal, ….. ) case class SubscriberServiceInfo[S<:Service,L<:S#Limits]( service: S, tariffPlan: tariffPlan[S,L], amountUsed:Double ) trait BillingService { def checkServiceAccess(u:Subscriber,s:Service):Boolean = u.serviceInfos(s).isDefined && u.account > 0
}
adding fields for Subscriber aggregates.
case class Subscriber(id, name, serviceInfos:Map[Service,SubscriberServiceInfo[_,_]], account: BigDecimal, ….. ) case class ServiceUsage(service, amount, when)
trait BillingService { def rateServiceUsage(u:Subscriber,r:ServiceUsage):Subscriber = serviceInfo(r.service) match { case Some(SubscriberServiceInfo(service,plan,amount)) => val price = …. u.copy(account = u.account - price, serviceInfo = serviceInfo.updated(s, }
adding fields for Subscriber aggregates.
trait BillingService { def rateServiceUsage(u:Subscriber,r:ServiceUsage):Subscriber = serviceInfo(r.service) match { case Some(SubscriberServiceInfo(service,plan,amount)) => val price = …. u.copy(account = u.account - price, serviceInfo = serviceInfo.updated(s, ServiceInfo(service,plan,amount+r.amount)) ) case None => throw new IllegalStateException(“service is not enabled”) } ……………. }
Subscriber aggregates [rate: lastPayedDate]
case class Subscriber(id, name, serviceInfos:Map[Service,SubscriberServiceInfo[_,_]], account: BigDecimal, lastPayedDate: DateTime )
trait BillingService { def ratePeriod(u:Subscriber,date:DateTime):Subscriber = if (date < u.lastPayedDate) u else { val price = ….. subscriber.copy(account = u.account - price, lastPayedDate = date+1.month) } }
Subscriber:
case class Subscriber( id : Long, name: String, serviceInfos:Map[Service,SubscriberServiceInfo[_,_]], account: BigDecimal, lastPayedDate: DateTime )
case class SubscriberServiceInfo[S<:Service,L<:S#Limits]( service: S, tariffPlan: tariffPlan[L], amountUsed:Double )
Subscriber:
case class Subscriber( id : Long, name: String, internetServiceInfo: ServiceInfo[Internet,Internet.Limits], telephonyServiceInfo: ServiceInfo[Telephony,Telephony.Limits], account: BigDecimal, lastPayedDate: DateTime ) {
def serviceInfo(s:Service):SubscriberServiceInfo[s.type,s.Limits] = ….
def updateServiceInfo[S<:Service,L<:S#Limits]( serviceInfo:SubscriberServiceInfo[S,L]): Subscriber = … }
From domain model to implementation. [S1]
Subscriber Service TariffPlan
Domain Data/ Aggregates Services
SubscriberOperations TariffPlanOperations ….
Repository
DD — contains only essential data
Services — only functionality
Testable. No mess with implementation.
Service calls — domain events
From domain model to implementation. [S1]
Improvements/Refactoring space:
• Errors handling
• Deaggregate
• Fluent DSL
Errors handling (design for failure)
trait BillingService { def checkServiceAccess(u:Subscriber,s:Service):Boolean
def rateServiceUsage(u:Subscriber,r:ServiceUsage):Subscriber
def ratePeriod(u:Subscriber, date: DateTime): Subscriber
def acceptPayment(u:Subscriber, payment:Payment):Subscriber
}
Design for failure:
trait BillingService { def checkServiceAccess(u:Subscriber,s:Service): Boolean
def rateServiceUsage(u:Subscriber,r:ServiceUsage):Try[Subscriber]
def ratePeriod(u:Subscriber, date: DateTime): Try[Subscriber]
def acceptPayment(u:Subscriber, payment:Payment): Subscriber
}
Design for failure:
sealed trait Try[+X]
case class Success[X](v:X) extends Try[X]
case class Failure(ex:Throwable) extends Try[Nothing]
when use Try / traditional exception handling?
Try — error recovery is a part of business layers. (i.e. errors is domain-related)
Exception handling — error recovery is a part of infrastructure layer. (i. e. errors is system-related)
Deaggregation:
trait Repository { def create[T](): T
def find[T](id: Id[T]): Try[T]
def save[T](obj: T): Try[Boolean]
}
Deaggregation:
trait Repository { def create[T](): T def find[T](id: Id[T]): Try[T] def save[T](obj: T) : Try[T]
………..
def subscriberServiceInfo[S<:Service,L<:S#Limits] (id: Id[Subscriber], s:S): SubscriberServiceInfo[S,L]
def updateSubsriberServiceInfo[S<:Service,L<:S#Limits] ( id: Id[Subscriber],s:S,si:SubscriberServiceInfo[S,L]): Try[SubscriberServiceInfo[S,L]]
}
Deaggregation:
trait BillingService { def checkServiceAccess(r:Repository, uid:Id[Subscriber], s:Service): Boolean
def rateServiceUsage(r: Repository, uid:Id[Subscriber], r:ServiceUsage):Try[Subscriber]
….. }
Deaggregation:
trait BillingService { val repository: Repository
def checkServiceAccess(uid:Id[Subscriber], s:Service): Try[Boolean]
def rateServiceUsage(uid:Id[Subscriber], r:ServiceUsage):Try[Subscriber]
….. }
// BillingService operations interpret repository
Deaggregation. [S2]
Subscriber Service TariffPlan
Domain Data/ Aggregates Services
SubscriberOperations TariffPlanOperations ….
Repository
Interpret - Not for all cases - Loss
- generality of repository - simply logic
- Gain - simple repository operations - more efficient data access.
DSL: Domain Specific Language.
Idea: fluent syntax for fluent operations. • Syntax sugar, can be used by non-programmers • ‘Micro-interpreter/compiler’ • Internal/External
Let’s build simple Internal DSL for our tariff plans.
TariffPlan: DSL
case class TariffPlan[Limits]( monthlyFee: BigDecimal, monthlyLimits: Limits, excessCharge: BigDecimal )
TariffPlan(100,Limits(1,100),2)
TariffPlan(montlyFee=100, Internet.Limits(gb=1,bandwidth=100), 2)
100 hrn montly (1 gb) speed 100 excess 2 hrn per 1 gb
// let’s build
TariffPlan: DSL
(100 hrn) montly (1 gb) speed 100 excess (2 hrn) per (1 gb)
trait TariffPlanDSL[S <: Service, L <: S#Limits] {
implicit class ToHrn(v: Int) { def hrn = this
def monthly(x: LimitExpression) = TariffPlan(v, x.limit, 0)
def per(x: QuantityExpression
}
1 hrn = 1.hrn = new ToHrn(1).hrn
trait LimitExpression{ def limit: L }
type QuantityExpression { def quantity: Int }
TariffPlan: DSL
(100 hrn) montly (1 gb) speed 100 excess (2 hrn) per (1 gb)
object InternetTariffPlanDSL extends TariffPlanDSL[Internet.type, Internet.Limits]
implicit class Gb(v: Int) extends LimitExpression with QuantityExpression{ def gb = this
def limit = Internet.Limits(v,100)
def quantity = x
}
TariffPlan: DSL
(100 hrn) montly (1 gb) speed 100 excess (2 hrn) per (1 gb)
(100 hrn) montly (1 gb) == (100.hrn).montly(1.gb)
TariffPlan: DSL
(100 hrn) montly (1 gb) speed 100 excess ((2 hrn) per (1 gb))
trait TariffPlanDSL[S,L]
case class PerExpression(money: ToHrn, quantity: QuantityExpression)
trait RichTariffPlan(p: TariffPlan[L]) { def excess(x: PerExpression) = p.copy(excessCost=x.v)
}
((100.hrn).montly(1.gb) speed 100).excess((2 hrn) per (1 gb))
TariffPlan: DSL
(100 hrn) montly (1 gb) speed 100 excess ((2 hrn) per (1 gb))
object InternetTariffPlanDSL[S,L]
trait RichTariffPlan(p: TariffPlan[L]) extends super.RichTariffPlan(p) { def speed(x: Int) = p.copy( monthlyLimits = p.monthlyLimits.copy( bandwidth = x) )
}
((100.hrn).montly(1.gb) speed 100).excess ((2.hrn).per(1.gb))((100.hrn).montly(1.gb).speed(100)).excess ((2.hrn).per(1.gb))
DSL: (100 hrn) montly (1 gb) speed 100 excess ((2 hrn) per (1 gb))
Internal
External
Need some boilerplate code.
Useful when developers need fluent business domain object notation.
Internally - combination of builder and interpreter patterns
Useful when external users (non-developers) want to describe domain objects. Internally - language-mini-interpreter. // [scala default library include parser combinators]
Post-OOD domain modelling
Domain Data Objects
‘OO’ Objects with behavior
• Closed world. • Different lifecycle can be described by different types • Composition over inheritance
Domain Services• Open world. • No data, only functionality. Calls can be replayed. • Traditional inheritance
Infrastructure Services• Interpreters of ‘domain services’ functions
// phantom types.
Thanks for attention.
Fully - implemented ‘tiny’ domain model and DSL:
https://github.com/rssh/scala-training-materials/tree/master/fwdays2015-examples/
Ruslan Shevchenko Lynx Capital Partners
https://github.com/rssh @rssh1