Many developers have a hate relationship with testing. However, I believe the main cause of that is code that is highly-coupled and difficult to test.

This post states some principles and guidelines that can help you write easily-testable code, which is not only easier to test but also more flexible and maintainable, due to its better modularity. Here at Feedzai, we try to follow these principles and guidelines in order to have better code quality, increased test coverage and confidence in the products we deliver.

This article mostly serves as an introduction to these concepts. If you want to understand these concepts better, you may want to do some additional research. The article explores the SOLID principles, the Law of Demeter, some other guidelines and ends with a small example that illustrates some of the aspects explored here.

SOLID design principles

One of the most well-known collections of principles in the software engineering industry are the SOLID principles, documented by Robert C. Martin (also known as Uncle Bob).

This collection of principles can help your code to be more modular and to have increased testability. Let’s go deeper into each of these principles:

Single Responsibility Principle (SRP)

Each software module should only have one reason to change.

So, what does that mean? Let’s take an example:

Imagine that you write a program that calls an external REST API endpoint, does some kind of processing with the received data and writes it to a CSV file. A naïve approach might be to have everything in the same class. However, this class has multiple reasons to change:

You might want to change the API call to a different provider or change it to read from a different source, such as a file.

The processing that you are doing might need to be changed.

You may want to write the results to a different output, maybe a different file format.

In addition, you might want to be able to have multiple types of inputs and outputs that can be interchanged in runtime based on some kind of configuration or input.

For these reasons, you should break your application down into multiple classes and interfaces. Here’s an example:

This principle can be applied at both class and method levels (and to some extent at the package level, but Robert Martin has specific principles for that).

However, you should be careful not to overdo it. The Single Responsibility Principle does not state that a module should only do one thing. It states it should have one and only one reason to change. For more information about this, please read this blog post from Robert Martin.

Open/Closed Principle (OCP)

Your classes should be open for extension but closed to modifications.

This means that your design should allow the addition of new features with minimal change to the existing code. This can be achieved by coding against abstractions instead of concrete implementations, as well as through the use of some design patterns such as Decorator, Visitor and Strategy. Following the Single Responsibility Principle also helps with that, as you will have things segregated.

The best way to think about this principle is to think about a plugin architecture. In such a scenario, plugins are developed to add behaviour to a system without changing any of its code.

Another smaller but more concrete example is Java’s Collections.sort method. It can sort any type of class that implements the Comparable interface without modifying the sort method for each new class. If you instead had a sort method with a switch statement with a case for every type you wanted to compare, you would have to modify it every time you needed to compare a new type.

For more information on this principle, you can read Robert Martin’s blog post about it.

Liskov Substitution Principle (LSP)

Objects of a superclass shall be replaceable with objects of its subclasses without breaking the application.

A good example for this principle is the square-rectangle problem: you might be tempted to have a Shape interface, a Rectangle that implements that, and a Square which extends it but guarantees that both height and width are always the same. This might look good at first glance. However, this example breaks the Liskov Substitution Principle because even though the programmatic API is the same, its preconditions and postconditions are not. If you have a class or method which accepts a Rectangle, you might not be able to simply pass in a Square in its place because the code might be assuming that the height and width can be changed independently. For the user of the interface, the implementation should not matter.

Another common example of breaking this principle is having implementations with methods that throw UnsupportedOperationException, which indicate that a certain operation is not supported in some specific implementations.

Interface Segregation Principle (ISP)

No client should be forced to depend on methods it does not use.

Large interfaces should be split into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them.

Following this principle helps to keep your system decoupled and makes it easier not to break the Liskov Substitution Principle.

For example, if you are creating an interface for a multi-functional printer, instead of having a single MultiFunctionalPrinter interface with a print() and a scan() method, you should instead have two interfaces: Printer and Scanner, each with the respective method. That way, if a client only needs the print() method, you can provide it with a simple printer without having to change any of the application code, as the client was not dependent on the scan aspects of the MultiFunctionalPrinter.

Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details. Details should depend upon abstractions.

Following this principle allows you to easily replace certain implementations with compatible ones which follow the same interface. This is very useful for testing as it allows you to replace real implementations with test doubles. It also allows you to better react to changing requirements.

The following diagram shows how the example presented in the Single Responsibility Principle section also follows the Dependency Inversion Principle. You can see that both high-level and low-level modules depend on abstractions.

Example of the usage of the Dependency Inversion Principle

For more information on the topic of abstractions, you may read this post by Gabriel Candal on the Feedzai TechBlog. 💪