This blog is part of a series explaining the SOLID design principles by looking at code from real projects or frameworks. Last time, we looked at the Open/Closed Principle. This time, we’ll discuss the Open/Closed Principle.
SOLID is an acronym for the following design principles:
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency-Inversion Principle
The SOLID principles are often explained by using simple examples, but that sometimes makes it hard to spot them in your own code for a real project. That’s why I searched for some examples of these issues in Open Source projects and use them in this series.
The Dependency-Inversion Principle
The Dependency-Inversion Principle consists of two rules:
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend upon details. Details should depend upon abstractions.
Before we look into the details of this, let’s first define high-level and low-level in this context. Low-level means closer to the user or metal (think of UI, I/O, network, storage etc.) and contains the details. High-level means the code is dealing with policies, business rules and the bigger picture.
Why do we want to avoid high-level modules being dependent on low-level modules? In general, low-level modules tend to change more often than high-level modules. Things like the user interface, the communication with web services, persistence regularly receive more updates than business rules and policies. When we make a change in a low-level module, we want to prevent that we also need to make changes a high-level module. In other words, we want to minimize the blast radius of those changes. Also, we want to be able to re-use the high-level modules (for example when we want to provide a new interface like a mobile app of API) and if we’re tightly coupled to low-level modules, this becomes a lot harder.
Here you can see a simple UML diagram of an example with and without Dependency-Inversion. Do you see how the arrow from the low-level module (Data Access) to the high-level module (Business) changes direction? And how both “Business Logic” and “SqlDatabase” point to the same abstraction? Instead of high-level code being dependent on low-level code with all its details, they are both dependent on an interface (the abstraction). Do note the abstraction that both the high-level and low-level module depend on is owned by the high-level module, not by the low-level module! Otherwise, the high-level module is still dependent on the low-level module.
Relation with Inversion of Control and Dependency Injection
Inversion of Control is related to Dependency-Inversion but is not quite the same. Inversion of Control (IoC) is a principle in which code receives the flow of control from outside instead of being responsible to create it itself. In the example picture above, on the left the Business Logic is responsible for instantiating the SqlDatabase. On the right, the Business Logic “receives” an instantiation of an implementation of IRepository, inverting the flow of control. Two ways to achieve Inversion of Control is by using Dependency Injection or the Service Locator pattern.
Dependency Injection is a way to achieve Inversion of Control and Dependency-Inversion. You “ask” for an instance of a class to your Dependency Injection framework and it instantiates one and will inject instances of classes that the class you’re asking for needs. Frameworks like Spring, ASP.NET MVC Core and others have a built-in Dependency Injector.
While Inversion of Control and Dependency-Inversion look similar (often the same examples are used), they are not the same. Inversion of Control doesn’t say anything about high-level or low-level modules and the direction of the dependencies between. You can perfectly adhere to IoC, but still violate the Dependency-Inversion Principle.
An example violation in the wild
This time let’s look at some code of WordPress, the popular CMS that is built on PHP. Like I said in previous posts in the blog series, it is not about me thinking this project has low quality. I selected it because the example was clear. Chances are that you’ll find potential issues like this in any reasonably sized project, either introduced consciously or unconsciously. That is one of the things that make Open Source great: we can be exposed to others’ work learn from it.
In WordPress, we have a function that allows you to post new blogs by mailing to an email address. WordPress checks the inbox for new mails and converts it to a new blog post. The module responsible for this is in the file wp-mail.php. In this module, a class POP3 is created to access the mailbox:
So the higher-level mail module is now dependent on a concrete low-level class! Think about what you need to do if you want to support a different mail protocol like IMAP. You then need to change wp-mail to handle that!
To solve this problem, you could create an abstraction IMailbox and depend on that. The POP3 class can then implement this interface and adhere to the Dependency-Inversion Principle. New implementations can easily be added without changing wp-mail.
This is not the only SOLID violation in this file by the way. The module has multiple reasons to change as well, thereby violating the Single Responsibility Principle. It is responsible for accessing the mailbox (first axis of change) and parsing the mails (second axis of change).
How to spot these kinds of violations
If you use Java or C# and you are working with separate modules/projects for the different layers, you can take a look at the dependencies. If you need to reference a low-level module from a high-level module, you’re in violation.
If you find it hard to add or replace a low-level part of the application (for example replacing the database or adding a different UI like a mobile app), it could also be a sign that your high-level modules depend on low-level modules, creating hard couplings.
Another way of telling that you have this problem is when it is hard to unit test a high-level component due to dependencies on concrete, low-level classes. This is in fact one of the biggest reasons people find it hard to start with unit testing or Test-Driven Development.
Although this one is not just for finding possible violations of the Dependency-Inversion Principle (also finds IoC problems and hard-coupled code), you can also look at all the code where you instantiate classes (instead of basic ones like string, lists etc.). In other words, search for the keyword “new”. This part of the code is not working with abstractions but is working with concrete implantation. Of course, you can never get rid of instantiating classes completely, but
How to solve these violations
You can solve these violations by adding abstractions (interfaces) and by using those abstractions instead of directly working with concrete classes. Make sure that the abstractions you create are part of the high-level module instead of the low-level module!
Although it’s not necessary, it helps if you use an IoC container/framework that does the instantiating and Dependency Injection for you. These frameworks also allow you to control other aspects, like the lifetime and concurrency of objects.
Adhering to the Dependency-Inversion Principle can help you to minimize the impact of changes and separates the areas of your code that change most often with the areas that are more stable. Again, it’s all about making changes more localized, allow for extensibility and reduce the chance of regression.
This blog was written by Harm Pauw.