Coding is hardly a “snap the finger” magical transformation. It needs a focused approach to learn coding and understand how this aligns well with unit testing.

Implementing Coding with Abstraction sets up a developer for the journey into application/class design because once you develop well-structured code, your thought process is more aligned to the design principles in application design.

So, what is coding by abstraction?

It encourages the developer to focus on the main logic in the flow being coded and ensure that the sub-flows are parked till the main flow completes – sounds “abstract,” doesn’t it?
 
Let’s take an example. Here, we are using java snippets for the examples below:
 
We have a service method that gets a http request, we need to enrich this with additional information, place this enriched message into a message Q and finally respond back to the sender. The assumption in this example is that each step is a significant unit of work. 

The distractions:

  1. Read the DB data and handle exceptions, maybe retry.
  2. Put the message into the message Q and handle exceptions, maybe retry.

 
Choosing the right approach

One approach is to code the above flow in the exact sequence one after another, switching back between functional and infrastructure code, diving into exceptions, retries etc. 

public Response processMessage(Message message) {
    sanitizeMessage
    getEnrichmentDataFromDB => throws exceptions without catching or wraps all in a custom app exception and throws back
    enrichMessage
    putMessageinKafka => throws exceptions without catching or wraps all in a custom app exception and throws back
    generateResponse
}

The developer has already baked in the notion that a database and Kafka will be used and codes the methods one after another assuming this will give them clarity on what needs to be handled in the following steps. But as you’ll see below, the design thought process does not start at classes and thereafter systems, it starts with code.

The “coding with abstraction” approach is to first focus on the main functional flow, abstracting out the infrastructure and sub-flow implementations such as reading from a DB, putting a message into a Message Q and additional processing for enrichment.

  1.  The first step would be to create the functional steps. Notice that we don’t mention how the steps might be implemented, 
    getEnrichmentData -> reads a DB
    enrichMessage -> performs data correlation and data merge operations
    sendMessage -> puts message in a Message Q
public Response processMessage(Message message) {
    sanitizeMessage
    getEnrichmentData
    enrichMessage
    sendMessage
    generateResponse
}
  1. Now define the handshake, i.e., what data is transferred between steps.
public Response processMessage(Message message) {
    sanitizeMessage(Message)
    ReferenceData getEnrichmentData(…)
    enrichMessage(Message,ReferenceData)
    sendMessage(Message)
    Response generateResponse(Message)
}

The next steps 

  1. You can now proceed to implement the methods from top to bottom but skipping the ones that need to incorporate infrastructure calls – getEnrichmentData and sendMessage. You’ll notice that unit testing the methods looks pretty straightforward. When Unit testing using mocking frameworks you can look at Mockito, for example, to mock out these two infrastructure methods and return responses, as required.
     
  2. At this point, you might already have written unit tests for all the other methods and the main processMessage method as well.
     
  3. Now to tackle the methods that need to access DB and Message Q, one approach is to start coding the DB calls into the methods. But let’s look at this more closely, using the getEnrichmentData method.

    By not explicitly indicating from where we obtain the reference data, we are setting the stage for definition of a DB implementation to obtain the reference data. At this point, it might be the only one, but that could change in future!

    Further looking at this, we could define an interface…
        interface DataProvider<R>
        {…}
        
        And…
        class ReferenceDataProviderDBImplimplements DataProvider<ReferenceData>
        {…}

 
The reason why you want to define interfaces and their implementations when working with infrastructure components is because you might need to change from, say, RDBMS to NoSQL DB or use a distributed cache.

You can take the same approach for sendMessage. This approach will enable you to structure your code development and unit testing. It also encourages good design while enrichingyour coding and design thought process.
 
With that,I hope now you will be able to implement a code development approach that encourages cleaner code structure and better unit testing.