- Published on
Functions, objects, actors: deliverying results
- Authors
- Name
- Damian Płaza
- @raimeyuu
This tale wouldn't be here if I didn't have conversations with Grzegorz Galezowski.
Thanks Grzegorz Galezowski for your patience, your knowledge and sharing your experience with me!
This blog post is full of reflections after reading the discussion between Grzegorz and Mark Seeman
An assignment
Imagine that we were challenged with implementing a simple use case in our system - an intent to assign a worker to a specific assignment.
Initially, we know that this use case is going to be realized using HTTP API, which means that our ASP.NET Core Minimal API handler or Controller's action method will be the entrypoint to our application.
The knowledge we are going to apply is the following:
- A consumer requests assigning someone to an assignment with given identifier.
- The system is going to fetch the first available worker.
- A specific assignment is loaded.
- The worker is assigned.
- The assignment result is returned to the consumer.
Straightforward, right?
(you might be wondering what do I meanby "the knowledge we are going to apply" - you might be interested in The ambiguity of application)
We defined inputs (the assignment identifier), the processing steps (2-4) and the output (the assignment result).
Let's try to implement it using one of the most common approaches - command handler returning a result.
Command handler returning a result
Let's start from the outside-in perspective - so the entrypoint to our system - an HTTP Minimal API handler - anonymous lambda.
In our initial behavior specification (specification? you might be interested in The ambiguity of "TDD") we covered the happy path. We have not describe what results do we expect when things go wrong.
Let's say our process experts told us that we have three failure scenarios:
- there is no workers available
- assignment has already assigned worker
- a worker is not qualified to do a given assignment
So we can represent this knowledge in the code:
We are engineers, so we know there the default set of errors to handle - "other errors" - ones we don't know how to deal with.
Let's put a error bag for these.
Our HTTP interaction handler uses the returned result from processing.
Of course, nothing yet was implemented, so let's move on!
Let's return a processing result!
Let's define inputs and outputs:
Ok, it got a bit verbose, but we have a clear structure of our processing result.
We expressed a common, abstract concept for "assignment result" and we have a concrete implementations of it, covering variability of cases.
I hear you dear Reader: "omg, so much boilerplate" and it's true, not denying it.
We could of course use OneOf nuget package for discriminated unions, but let's imagine we decided to go with dependency-free approach that is built in the language we use.
Processing knowledge application
Now it's time to build command handler - an application of the process steps we got familiar with.
One of them was: "2. The system is going to fetch the first available worker."
We don't care how this capability is going to be delivered, we focus on what instead - to do so we defined the responsibility: (responsibility? you might be interested in The ambiguity of interfaces)
Our command processor requires capabilities for loading and saving assignments, so let's express them bound to a specific role:
Equipped with those capabilities, we are ready to represent our understanding using code:
(not sure if you noticed, dear Reader, but I used a defensive, "functional" way of using Maybe type - if you are interested in this topic, please check Map, don't ask)
Phew, that was a bit exhaustive, but we managed to reach our goal!
We check our behavior specifications, also known as tests - it's all green ✅ (we have tests, right?)
We are using C#, object-oriented language, so this "object" (ok, I know, it's a record, but bear with me) does all the processing.
One observation might be that it's violating CQRS as it returns something, and we all know that it's either state changing or answering about state, never both, right?.
But is it really an answer? Or maybe an acknowledgement?
So our little "object" provides processing result, a value related to state of the processing, not the state of the system.
And how this processing result is consumed? As it is returned from the command handler Handle method, the command handler does not give a damn how it's used.
For command handler, it's transparent, context-free.
Let's look closely on the signature of the method:
As you might recall dear Reader, we explored similar scenario in I, interface, where we changed the sequence of how inputs and outputs appear in the signature, creating a function:
This is how a function signature could look like in F#.
One could say that our command handler with its Handle method resembles a function - it accepts inputs, it uses capabilities provided by other objects, it returns a processing result as an output.
This is one of the most common approaches to handling commands - kind of a functional-style.
The way functions deliver "answers" is by returning a value.
What about objects?
"Telling" a result
One of the core aspects of object-orientation (which is a misnomer on its own; if you are interested in that topic - explore The ambiguity of objects) is sending and receiving messages.
Pointing at great, wise words by Sandi Metz:
You don't send messages because you have objects, you have objects because you send messages.
Let's ask ourselves - is AssignWorkerCommandHandler sending messages?
In a way, yes - it asks other objects to provide answers and based on those answers it decides what to do next.
It also receives messages - actually one message - AssignWorkerCommand message.
Turns out that message-passing collaborators, which can be another name for "objects", could be more fond of telling others what to do, rather than returning anything.
Message-passing is the key component here.
And this is exactly what Grzegorz presented with his solution.
It's a pure "Tell, Don't Ask" approach for designing interactions between objects - which means that in order to deliver processing results, objects need other objects to fullfil such responsibility.
If we look closely, one of the responsibilties our command handler has is deliverying "an acknowledgement", processing outcome.
What remains the same - deliverying results, how might vary.
One of the possibilities is returning the result so that downstream consumers can decide what to do.
Another one is closely related to "reporting results back" by a specialized role - "a result collector" (if you want to understand more about roles, please check The ambiguity of objects and The ambiguity of intefaces).
Shall we try using "Tell, Don't Ask" in this little problem space?
We need to change our command handler Handle signature, from this:
to that:
"But where is the result?!", an avid Reader might be wondering.
It will come, no worries.
Responsibilities does not disappear, they are just assigned to other roles.
Now let's move to our HTTP interaction handler:
Note dear Reader that it is one of the possible implementations and it varies from what Grzegorz suggested.
Ok, let's get back to our command handler!
This implementation of results collector is capable of being told what to do (a specific reporting) and provide answers when someone asks (using Match method).
So in a way it behaves both as an object (receiving messages) and a value object (that can be "mapped" into something else).
Now, command handler tells to resultCollector, being responsible for collecting a result from assignment processing, what to report.
AssignWorkerCommandHandler knows what happens when, and a someone who plays a role (that is able to report things back) will make it happen.
We saw WorkerAssignmentResult in action, so let's see how does he fullfil his responsibilities:
For the AssignWorkerCommandHandler it is transparent and unknown who satisfies "reporting capability".
It could be a dedicated object (WorkerAssignmentResult), a controller (as in Grzegorz's solution), or any other object that is able to respond to a call (response-ability, right?).
We've made a "little shortcut" and reused AssignmentResult value object from previous example, but still - wouldn't it be neat to have it?
Getting back to our command handler - it is a truly message-passing collaborator - it plays a major role in interactions with other objects.
If again look closely into Handle signature - it changed.
From types perspective, we don't have AssignWorkerCommand => Task<AssignmentResult>, but AssignWorkerCommand => Task instead!
Which initially might seem counterintuitive and against the natural way of thinking that people have, in which we tend to expect things to "flow" from top, to bottom, from left to right.
And here we throw things into void?
Do you know, dear Reader, what is the best way of checking the quality of our designs?
By introducing a change and see what is left when things are collapsing.
#1 Change, change never changes - "functional" command handler
Imagine that we were challenged to implement logging for each not-so-happy-scenario in assigning worker use case.
How does this affect our "functional" command handler?
As a result, we also need to adjust a bunch of tests, as we changed not only the Handle method, but also a constructor.
"You could add this logging to controller, dumbass" - one might think.
Of course, we could assign the responsibilty for reporting logs to controller. Then we could report only failure scenarios there.
No doubts there.
But still we would need to add logging to only failure branches of the match, isn't it?
Ok, let's move on.
How will message-passing, collaborative command handler handle that requirement?
#1 Change, change never changes - message-passing, collaborative command handler
Of course it vastly depends on the implementation - but in fact it might not change at all!
We need to adjust one of its collaborators - someone who plays a role having the responsibility for collecting assigning results - ICollectAssignmentResult.
This causes a ripple during the HTTP interaction handling, let's fix that:
As you probably noticed dear Reader - there is no magic, we need to touch some elements of our design.
This also might require adjusting some of the behavior specifications (tests), but on the different level.
Question is what we already alter - "logging" is pretty frequently happening capability, so it might have been added from the day one.
Although, one of the the reasons behind "logging" might be "reporting results", and in this scenario it was a breeze to be added in collaboration-based solution.
Functional-based solution got a bit "cluttered" with logging.
Some people are used to such appearances, so it does not seem "strange".
From objects to actors
By clearly segregating responsibilities for processing and for collecting results, we achieved some interesting qualities.
When we moved from "functional" to "message-passing collaborator", our command handler become something different.
It was still able to deliver the same capabilities, although it required help from others, so true power emerged from the collaboration, not objects acting alone.
Composing behaviors around reporting capability was already baked in, due to objects interacting with each other.
Don't get me wrong, dear Reader, I am not bashing or complaining on functional programming, as I am really into it.
Turns out that our AssignWorkerCommandHandler started looking more as an actor, rather than "a function".
Look what a simple act of changing how processing results are reported changed the nature of the handler.
It did processing and sent messages to others.
As we explored in The ambiguity of objects:
Transformations and message passing are two sides of the same coin.
Some might comment that it got too boilerplate-y, less terse and less readable.
Well, maybe.
There are systems built on top of the actor-model and prove their reliability, robustness and abilities to respond to a change in a swift fashion.
Initially, it might feel strange to "report back" results, instead of returning them.
It's a matter of getting accustomed to it.
But look on the bright sight - what qualities such design gives us back?
Imagine what superpowers you might get when you truly embrace actor-model, e.g. by using Akka.NET.
Bonus: #2 Change, change never changes: notifying about successful processing
As a bonus, imagine that there is another requirement to incorporate into our system: whenever a worker got successfully assigned, we should notify interested parties.
It should be done by sending a message by utilizing a queue.
If you were constrainted to use a message-passing collaborators approach - how would you design it, dear Reader?