How abstraction can reduce complexity, and increase testability and maintainability

When I was a junior developer I had no clue what I was doing and I had no one above me that guided me in the right direction. All I had was a lot of confidence (that I was the best software developer in the world) and a lot of good intentions.

Since one of my worst traits is my stubbornness, this is most likely the best training I could have ever had. I have made all programming mistakes at least once, and probably implemented all anti patterns at least once. Although I would always have a working product at the end, the code was not as graceful as it could have been. I could also have implemented it one eight of the time or less if I had the right tools under my belt.

One of the most valuable lesson I ever learned is how to use abstraction to reduce the complexity of my code. Maintainable code equals testable code equals quality code equals extensible code. Abstraction helps you increase the testability of your code, so that also means that abstraction can help you increase your code quality.

The lower your cyclomatic complexity, the more maintainable your code, so therefore the higher the quality. The cyclomatic complexity is a term that is often perceived as something very difficult to understand. And if you are like me I think it is if you google for it:

Cyclomatic complexity is a software metric, used to indicate the complexity of a class, file, method or application. It is a quantitative measure of the number of linearly independent paths through a program’s source code.

Essentially the easiest way to explain complexity is to count all the branches in your code. A method that immediately returns something has a complexity of 1.

Now, for every branch of code you have, add 1. The following method has a complexity of 3 as there are three independent paths that the program can follow.

A very simple rule of thumb is that every branch in your code adds a number of complexity. The same goes for loops, case statements and special constructs. The higher the complexity the more difficult it is to maintain your code. Also, it is now more work to test your code. If you aim for 100% coverage, you need to write 3 unit tests for this code, as you would need to cover all 3 of the situations (or states or branches or paths).

Now, imagine that we are creating a traffic light, and that the traffic light has 2 more methods in which the state determines the value it needs to return, for instance GetDuration() (indicating how long the light should be red, yellow or green), and SwitchState() which will make sure the system goes to the next state. So, we now have three methods with a complexity of three. If we need to add a new state, all methods will have a complexity of 4, and we would need to make changes in all 3 of the places. Granted – this is a simple system, but this this is exactly how the code becomes less maintainable over time. Also, it is easy to forget to add the extra case statement in one of the methods. The general rule of thumb here is the rule of 3. If you can reuse it more than three times it is time to look for an abstraction.

It is easy to see that in order to reduce the complexity we need to create an abstraction of the methods that have the high complexity.

Using this abstraction we can delegate the complex methods in the TrafficLight class to the abstraction. So we can reduce the complexity from each of these methods from 3 to 1.

You can never really get rid of the complexity completely in your code, but you can reduce the complexity per class. Now we have 3 implementations of the abstraction that contain a complexity of 1. So the overall complexity of your application stays the same, but your maintainability goes up. We now have written extensible code in a way that is a lot easier to test – the test code is now also less complex.

The implementations for YellowState and RedState are obviously very similar. Please download both the before and after code samples here.

Conclusion

For the readers that have been paying attention we have now reduced the complexity, and increased the maintainability of the TrafficLight class using the Gang of Four State Design Pattern. The number of unit tests for the TrafficLight class should be reduced as we would now test the RedState, YellowState and GreenState classes individually, which would only require one-liners to test them, which arguably may not be required at all. We are now conforming to a number of the SOLID principles; each state implementation does 1 thing only (SRP); the traffic light class delegates its calls so it is fit for adding an endless number of states (OCP). and we now code against abstractions rather than concrete implementations (ISP).

Note that this implementation of the state pattern is a very basic one, and does not limit itself to simple Queries and a single Command. In a lot of situations you can use the state pattern to act differently in different situations or states. The classic example is the Play button of a music player. It is ignored when it is playing already, but will start the playback of music when rewinding, paused or forwarding.

If you have any questions or comments please leave them below!