Are functional domain models always anemic?
Domain Driven Design advocates against the use of anemic data models and for the use of rich domain models. DDD is said to be technology-agnostic, and thus also programming-language-agnostic. But does this work in practice with functional programming, where all logic lives in functions and not in classes?
What is an anemic data model?
An anemic data model is a representation of domain objects that does not contain any business logic. In OOP terms, this means that the classes of this model contain only fields and their respective getters and setters. This puts the burden of correctness (i.e. which states are valid and which are not) to the clients of the model… to all clients to be precise. In the best case, this distribution of logic leads to duplicate efforts when the business logic has to change, but this can quickly grow into an unmanagable ball of mud.
Rich domain models, on the other hand, encapsulate the logic into meaningful objects and methods that do not allow clients to change the data as they see fit. Well, objects and methods are what OOP-style models use. But what if we wanted to take a more functional approach?
Functional Programming and logic
In functional languages like OCaml (which I use for the examples below), we compose functions and a handful of base data types to build our logic. The data structures themselves do not (cannot) contain logic. How do you protect your model from invalid changes that go against the business rules? Let me show you with an example what I mean by this.
Example: An anemic, functional model
Here is some OCaml code for a simple study group management domain in a file called study_group.ml
.
I’m not going into the details of the OCaml language and hope the example code is understandable.
The add_student
function contains a business rule:
A study group can only contain 5 student.
When the study group is full, the function returns a domain-specific response; a type called Study_Group_Is_Full
.1
|
|
A client of the code can use the add_student
function like this (in a different file called client.ml
):
|
|
This works as expected and the res
variable contains Study_Group_Is_Full
, but there is a problem:
Any client can just bypass the domain logic by adding a student to the study group directly, using the list concatenation operation ::
, like this:
|
|
How can we make sure the business rule is always executed?
Slightly changed model
We have to hide the fact that a study group is just a list of students from the clients and expose only those functions that comply with our business rules to clients.
To do this in OCaml, we need an additional file for our interface (with the same name, but different file extension: study_group.mli
).
|
|
This file contains the student
type as well as the study_group
type.
But we do not define the internal structure of the study group, merely “mention” it2.
We also do not include the function bodies, only the signatures of the functions we want to expose to outside clients.
Now, we just need to make slight changes to our implementation:
We introduce the study_group
type and change the function signatures accordingly.
We now also need a “constructor” function as we cannot create a study group on the client side otherwise.
|
|
When we now try to use the contatenation operator ::
to append a student to a study group (full or not), we’ll get an error as the types study_group
and student list
are not compatible.
The only way to add a student to a study group is through our add_student
function and there is no way around our business rule.
Encapsulation
This example shows that a functional model does not always mean the model is anemic. We used the same principle that OOP languages use to hide information and enforce business rules: encapsulation. It’s just that this kind of encapsulation looks different. By making deliberate choices of which types and functions we expose to outside clients, we can get the same benefits as encapsulation does in OOP languages.