In this short post, I want to demonstrate an approach to decouple a developed system from external messages that it should handle. Our goal is to fulfill functional requirements and keep the developed system flexible enough to add new features as effortlessly as possible.

The project

Incoming messages will contain a resource id, some flags, and a list of checks to perform against that resource.

Those checks can be costly, and some messages will carry the "validation" flag to give us a hint when we can skip them. Should we encounter it in a message,  we will evaluate the worthwhileness of running each check.
If a message doesn't have it - it's assumed to be worthy of processing.

Validation flag also alters output behavior - if it's enabled, we are always obligated to publish a result. If not, and all checks failed - it is considered useless, and we don't need to publish anything in response to it.

The first pitfall: coupling with an upstream data source

First off, we want to be in "partnership" relations with upstream data service.
That means we don't want to be  dependent on the decisions that they make and the sure way to do it is to decouple our code from the shape of the structure that they control, and we don't

Assume that message that we ingest and the message that we will produce are two different beasts.

We are not going to extend the original message with our fields. Instead, we are going to create a new one.

The names are contrived on purpose, but I hope they will illustrate the relationship though.

The second pitfall:  drawing border in a wrong place

Here is how we will process an incoming message (%Apple{}) - we will take checks from, iterate through them one by one, process, gather results, and put them into an outbound message structure (%AppleJuice{}).

Next - the code that will support that behaviour:

Now we are completely protected from any changes to the data structure!
But isn't there something irritating with all those functions inside Check module and frivolous treating of the structure itself -  sometimes, we accessed its properties through functions and sometimes directly?

Indeed we pushed the border way too far south!

The SRP principle tells us that module should have only one reason to change. And fortunately, we already established that reason in the first section: change of the shape of the data structure shouldn't cause us to change our code.
The Check struct here serves as a miniature "anti-corruption layer," and we will draw the border between fields that we control in it and those that we don't!

The only functions that should stay in Check are those that access the data source field - the data that we don't have any power over.

Notice less mouthful and how much more pleasant to read it become!

We also kept the new and unwrap functions in the Check module. Creating  Factory and  Formatter doesn't make any sense in our case, and they can stay there for a while :)

Things to consider

Think of  the following new requirements:

• publish results to a queue communicated to us in %Apple{}:
• add a validated property to action's result if an incoming message has to be validated

Will the  structure that we laid down help you accommodate them, or will it get in your way?

Summary

"Good fences make good neighbors"   because borders separate what you control from what you don't control.

Now you have one more heuristic to apply SRP!