placeholder

IF is short for Interface

author

Martin Goodwell

August 10, 2022

How to make sure cluttered codebases become a thing of the past

One of the main reason for code becoming rotten over time is that the codebase becomes cluttered. Logic is spread all over the place and adding new functionality becomes an increasingly bigger problem.

This blog post describes one major contributor to cluttered codebases and explains how you can make sure it doesn’t happen.​​​

Imagine a system consisting of five major building blocks dealing with data:

  • Input
  • Process
  • Persist
  • Query
  • Output
Five concerns of a software system, each being worked on by dedicated teams.

Let’s assume these five blocks are being worked on by five different teams and they are mostly used in a cross-cutting way. So, whenever data is entering the system, it gets handed through all the blocks before it goes out again. Output can happen via UIs or APIs but that doesn’t matter in this context.

We want to concentrate on the fact that there’s different types of data, and each type is handled differently by all these blocks. As software development goes and the system evolves, a typical implementation would look like with an example.

The quick and easy way

In the example below, the Input class defines the next two steps: processor and persistor. It also orchestrates how data flows between them.

class Input {
Processor processor;
Persistor persistor;

void input(Data data) {
processedData = processor.process(data);
persistor.persist(processedData);
}
}

In the Processor class, we check the data to find out which way of processing is required.

And we call the right method, according to the color.

class Processor {

ProcessedData process(Data data) {
if (data.isGreen()) {
processedData = doGreenProcessing(data);
} else if (data.isBlue()) {
processedData = doBlueProcessing(data);
} else if (data.isOrange())
processedData = doOrangeProcessing(data);
}
return processedData;
}
​​​​
}​​​​

The same goes for the Persistor class: we check the color and call the right method.

class Persistor {

persist(ProcessedData data) {
if (data.isGreen()) {
doGreenPersisting(data);
} else if (data.isBlue()) {
doBluePersisting(data);
} else if (data.isOrange()) {
doOrangePersisting(data);
}
}
}

So far, we have three different types of data that can be handled by our system. As you have surely recognized, we’re making the same decision about colors in two different places. In real life, these types of situations aren’t always as clearly recognizable as in this example, especially because these pieces of code are written by different teams.

If this codebase grows over several years, we might suddenly decide to introduce a new color and do it fast. So, there’s no way that five different teams (one for each block) could work on this new color in a coordinated manner to get it done in 6 months.

The solution: call the Tiger Team and ask them to implement the new color.

In reality, there’s no Tiger Team to call. You’ll have to recruit people from existing teams to work on this. And this new team will need to dig into the code of five different areas, written by five different teams, to find out how to add a new color to each of them.

The truth is that it’s not always as simple as shown in the previous code samples.

How can we structure a codebase to be ready for growth?

The answer is in the title of this blog post: identify these IF decisions that span across the codebase and leverage the power of polymorphism.

Polymorphism

class Input {
Strategy blueStrategy = new BlueStrategy();
Strategy greenStrategy = new GreenStrategy();
Strategy orangeStrategy = new OrangeStrategy();

void input(Data data) {
if (data.isBlue()) {
blueStrategy.handleData(data);
} else if (data.isGreen()) {
greenStrategy.handleData(data);
}…
}
}

The Input class is still responsible for identifying the different types of data and it initiates the dataflow.

However, this time, we introduced dedicated classes for each color. And they all implement the same Strategy interface.

class BlueStrategy implements Strategy {

Processor blueProcessor;
Persistor bluePersistor;
Queryor blueQueryior;
Outputter blueOutputter;

void handleData(Data data) {
ProcessedData processed = blueProcessor.process(data);
bluePersistor.persist(processed);

​​​​​​​ ...
}
}

We still need to implement all the classes for processing, persisting, querying (of course Queryor is a word — and if wasn’t been before, it is now), and outputting of the data.

The difference is that all of them now follow the same interface. And the way data travels through our system is now clearly defined and specified.

Benefits

This does several things for your codebase: First, it clearly defines everything that can happen to data in the system: Persisting, Querying, Processing, etc. Not only does the system become easier to understand for engineers joining a team, it also automatically guides every team to have the same perspective on the big picture.

By just using if-statements, the concepts of blue, green, and orange are cluttered all over the codebase. You won’t find a single point of responsibility that deals with the essence of blue; you will have to find your way across multiple if-statements.
So, while blue, green, and orange are central aspects of your system, they aren’t represented anywhere. Polymorphism provides you with exactly that: dedicated classes dealing explicitly with these core aspects.

Also, this makes it much easier to share growing codebases between members or even teams. It might have been really easy back in the old days when only a dozen of developers needed to coordinate their efforts. Every team working on their dedicated block could just work in sync with the other blocks to implement new features.

Years later, a different constellation of people might need to work on new features. One single team might need to cut through all five blocks to introduce a new color. Or a new way of persistence might be introduced, and we need to add this to all existing colors. Existing interfaces make all of this easier.

High-performance side effects

Last but not least, if you just don’t care about the dynamics of growing and changing teams, you might still care about that last bit of performance: CPUs of all colors love code without if-statements.

That’s because execution pipelines are increasingly efficient with less branch prediction (see https://www.baeldung.com/java-branch-prediction for more information on that).

Summary

So, whenever you’re finding that your codebase is doing the same checks over and over again, take some time to look into it. Maybe you have found a hidden gem in the form of multiple if-statements that might be turned into an Interface of central importance.


IF is short for Interface was originally published in Dynatrace Engineering on Medium, where people are continuing the conversation by highlighting and responding to this story.

Written by

author

Martin Goodwell