Functional core & Imperative shell: explanation with code
In my last article I introduced a design technique called Functional core and Imperative shell (let’s call it “FC&IS”). It enables to do isolated testing without using test doubles (mocks, stubs). It’s also said to produce better designs (I’ll let you judge of that).
My post was a bit abstract and introduced quite a bit of vocabulary. Now I’m going to make it a bit more real by showing some code. Writing this code also helps me re-arrange and crystallize my understanding. Here we go!
For the sake of conciseness and clarity, I will write pseudo code using Moops-like syntax.
Entity - Service interface
With FC&IC you want to send some $data from the entity to the service, so you write this:
my $data = $entity->compute( $data_in );
$service->perform_with( $data );
The entity passes some $data to the service.
Without FC&IC, I might have written this instead:
$entity->do_it_with( $data_in );
class Entity {
has service;
method do_it_with ( $data_in ) {
# Some code which involves $self->service
}
}
I often end up writing this second type of code when I “feel” like the service is part of what the entity does. For instance I would “feel” that a User::Email::Sender “has” an Email::Stuffer (who will ->send() each email). It needs it to send_late_payment_email_to($user)
, right?
One problem with that approach is the consequence for testing. To test the User::Email::Sender, I now need to check whether the Email::Stuffer did ->send() some emails, and perhaps what the content of these emails was. I will need a mock.
On the other hand, in the FC&IS code I only need to test that I’m sending the right data out to the service. The code looks like this in this case:
my @emails = @{ $user_email_writer->late_payment_emails_for( \@late_payers ) };
my $email_sender = Email::Stuffer->new;
$email_sender->to( $_->to )
->body( $_->body )
->send
for @emails;
How to test User::Email::Sender in isolation? @late_payers
in, @emails
out. As simple as that.
In this code the class also has a much clearer interface with Email::Stuffer. The interface is simply the @emails (which are a ValueObject: they do nothing else than carry information around). Values are the clearest interface.
And well defined interfaces are the maintainable glue of well designed code: they enable one entity to vary without impacting the others.
In the non-FC&IS code, the Email::Stuffer could be used in various places inside the User::Email::Sender, the interface between the two is unclear. That makes it really easy to add more and more coupling, until they become entangled.
Well… in this particular case we’re only ever going to call the Email::Stuffer once for each email, so things can’t really become too tangled. But speak about drawing a house (a square and a triangle). You want to pass in only one place, all the lines that the Line::Drawer must draw. You don’t want to have a Triangle::Drawer class call the Line::Drawer in various places in its code (once for each line to be drawn), and then do the same in the Rectangle::Drawer. That would be coupling the calculation of shapes, and the drawing. The solution is simple: pass the relevant information from one to the other; a list of lines to draw.
I wish I could think of a better example where service and entity would get tangled together. If anyone can think of one… (unfortunately I cannot share the code shown in Gary’s two videos, because he sells these)
Entity - Entity interface
It’s less clear for me from that video (and from my design insight), but I think that the same principle holds between Entities: they should send data to each other, not be part of each other.
FC&IS code:
my $data = $entity_1->compute_1($data_in);
my $result = $entity_2->compute_2($data);
$entity_1
passes some $data
to $entity_2
.
This code is very simple to test, and the boundaries are well defined.
Another implementation could be:
my $result = $entity_2->compute($data_in);
class Entity2 {
has entity_1 => ( isa => 'Entity1' );
method compute( $data_in ) {
my $data = $self->entity_1->compute_1($data_in);
return $self->compute_2($data);
}
}
In this code, to test compute()
I will need to stub compute_1()
: make it return a “canned” $data
, so that compute_2()
can be exercised.
Yet another implementation
Or the relationship could be the other way around:
my $result = $entity_1->compute($data_in);
class Entity1 {
has entity_2 => ( isa => 'Entity2' );
method compute( $data_in ) {
my $data = $self->compute_1($data_in);
return $self->entity_2->compute_2($data);
}
}
In this case I end up stubbing compute_2(), which is even weirder.
This is not satisfactory. What we want to test in this case, is that compute_1() works properly. It’s the only thing that Entity1 does itself, in the end ! We need to test these things in isolation. And for that we need them to be clearly isolated: with Values as boundaries.
I have been a victim of “has” many time: too often, I think that some object “has” another one. Most of the time these objects are performing different behaviour, and what they need is simply to pass data to each other.
Design rules
The design principles are pretty simple in the end:
- no Entity should have a Service as an attribute
- no Entity should have another Entity as an attribute
- only data (ValueObjects) gets passed between Entities, and to Services (and it should be immutable)
- this data passing happens in the Imperative shell
The Imperative shell can either be a script, or a set of methods in some object. As long as it respects the rules above.
For bigger systems, several Imperative shells can send values to each other.
For asynchronous systems, the same principles still hold. In one of his videos Gary has some asynchronous stuff going on. He receives data from the network through a queue, and somewhere else puts data in a database asynchronously. He says that because all values are immutable, race conditions are impossible. I must say I don’t speak “asynchronous” very much myself, so I will let you draw your own conclusions.
Discussion
What I find confusing when I try to think about this design using Perl, is that the Entities could be just functions or methods, rather than classes. An entity is just something that takes data in, performs some behaviour, and sends data out. Just like a function or method. For instance the code for our billing system could look like this:
my @late_payers = @{ list_late_payers( \@users ) };
my @emails = @{ write_late_payment_emails_for( \@late_payers ) };
send_emails->to( $_->to )->body( $_->body )->send for @emails;
That looks much less OO, and more Functional (or even Procedural, in this simple case). That’s why it’s called “Functional core”
According to Gary (who came up with FC&IS), manipulating business logic is what Functional Programming is good at, while OO is better suited for representing real world, mutable objects. Hence we end up with a functional core and an OO layer to the outside.
The role of Classes in the functional core, seem to be just some sort of namespace, to keep different concerns in different files. They are still very useful.
In Entities, attributes can hold some (immutable) values who are useful for several methods, and who make sense in terms of the business domain.
And for Values it’s important that I can ask a Rectangle for its $rectangle->perimeter
. Writing perimeter_of_rectangle($rectangle)
every time is not an option. And I don’t want to start importing functions in the imperative shell.
So I would still use Moo, both for Entities and ValueObjects.
This is still a tad confusing for me. No, really, I must start using it soon so I can know how it works when you’re doing it.
Let’s conclude.
I have been exploring my understanding of FC&IS, how was it for you?
Have you gotten any good insight out of it? Do you think it could help you test better, or design better?
Do you think this could be easily implemented in Perl? (in particular immutability)
Do you think it should?
I re-watched the videos, and realized that I forgot to mention something in this post: while the Functional core has the intelligence, the Shell has the State.
For instance the Shell can keep track of where a cursor is on the screen, while the Functional core will provide the mechanisms to make it move (the cursor itself is immutable - it's the shell who will assign a new value to the cursor, when it moves:
# This happens in the Shell
$self->set_cursor( $self->cursor->down );
To be properly decoupled, the Core is strictly wrapped into the Shell: "the core only interacts with the core, and the shell organizes the core along with the stateful pieces of the system."