As an object-oriented programmer, the transition to Scala and functional programming has been a rollercoaster of emotions. As I ride along, my intention is to write about my observations and what I have learned. The first obvious difference, and one that has been a bit hard to swallow, has been the rise of the anemic domain model.
What is an anemic domain model? Let’s start from the beginning
When translating models into code, the object-oriented (OO) way is to couple behaviour together with state in order to create a domain abstraction. If we take an aggregate as an example (if this word is new to you I suggest reading up on the tactical patterns of Domain- Driven Design); An aggregate is considered a consistency boundary where the root entity ensures consistency by encapsulation of the underlying concepts forming an intention revealing interface. An Account might expose methods like credit or debit which will execute behaviour with a result in a state change of the underlying objects such as the Balance.
With the previous example, we are on our way to create something called a rich domain model where all the business objects clearly expresses behaviour. The opposite of a rich domain model is the anemic domain model. The characteristics of an anemic domain model is the lack of behaviour exposed by the business objects. Instead we find the logic hidden within other services (or even in the presentation or data layer) and the business objects themselves are only data, usually mapped 1-1 with the database tables. We are aiming towards the rich domain models because they express intent, ensure consistency and are independent of infrastructural concerns.
When entering the world of functional programming, the first thing I encountered was the rise of the anemic domain model and suddenly the rich domain model was considered an anti pattern; Here’s why:
With functional programming we are using immutable objects and pure functions. An immutable object cannot be changed after creation and a pure function is always returning the same output given the same input. This might seem a bit familiar to what we learned in math; y = f(x), which means that each input x will always correspond to the same output y. Another thing we learned in math was that we are able to compose functions; y = f(g(x)), which means that the output of g(x) will be the input to f(x) but it still keeps the same properties where each input x always correspond to the same output y.
When building a functional domain model we want to leverage the usage of functional composition and we do this by dividing the concerns of state and behaviour. The resulting model will become anemic, but this gives us some interesting side effects.
- State can now be modelled as an algebraic data type (ADT).
- Behaviour can now be Composable
To keep it simple; an ADT is a type combined of multiple fields.
case class Account(no: AccountNumber, holder: Holder, balance: Balance)
case class Balance(amount: Amount, currency: Currency)
Account consists of an AccountNumber, Holder and Balance, where the Balance consists of an Amount and a Currency. An ADT does not contain any behaviour, they are immutable and we create them out of our ubiquitous language. As a bonus you can now see the small amount of code needed to create entities and value objects in Scala.
By removing the methods from the aggregate and reimplement them as functions, we have the possibility to use functional composition in order to combine and chain these functions. This is beneficial for us when building complex business logic. For example, withdrawal at certain ATMs is a composition of two debits one for the actual amount and one for the transaction fee.
def debit(amount: Amount)(account: Account): Account = // implementation omitted
def withdraw(amount: Amount, fee: Amount) = debit(fee) _ compose debit(amount) _
val account = withdraw(amount = Amount(100), fee = Amount(5))(myAccount)
We now have a withdraw function! If you have f _ compose g _ the result will be f(g(x)) which means that the debit of the amount will be computed first followed by the fee resulting in a Account with the two debits present. This is just a simple example but the more you think about it, everything can be described as a set of composed functions. e.g. A cup of coffee is a composition of grind and brew.
From a modelling perspective this has a few benefits. When talking to domain experts we are seldom interested in the current state of something, we are interested in why that thing is in that particular state. By using functions as the first class citizen we have a building block that communicates the why of our model.
From a technical perspective this also has a few benefits. Immutable objects are thread safe by design and by creating strong types we have a better way of reasoning about our data. Pure functions are just amazing to test; same value in always give the same value out which opens possibilities for property based testing. Functional composition lowers the cognitive load by abstracting the how and expressing the why showing a clear intent of the code written.
I am a fan of rich domain models. The encapsulation helps to abstract the domain concepts keeping everything at the same place. Functional programming is still new to me, and we will see if it makes me go all the way over towards the anemic side. It has me moving in that direction, especially with the benefits and simplicity introduced by pure functions and algebraic data types.
Join me for the next step in my journey where I will talk about decoupling side effects using Read Monads.