Effective Program Structuring with the Dependency Inversion Principle
How Abstraction and Inversion Produces More Flexible Code
In the previous four parts of the SOLID series we discussed how to compose object-oriented code that is flexible, maintainable, and reusable. Achieving this goal requires careful attention to how a particular entity (class, module, function, object, etc.) does its work. A significant consideration in this regard is that of dependencies. Does your entity depend on another entity to do its work? If so, how tightly coupled are the two? Will changes in one cascade into the other? These are important questions, which we discussed in part when reviewing the Open/Closed Principle and the Liskov Substitution Principle; however, dependency organization is an issue that warrants closer examination. That’s where the final SOLID principle comes in: the Dependency Inversion Principle (DIP).
A Quick Refresher on SOLID
SOLID is an acronym for a set of five software development principles, which if followed, are intended to help developers create flexible and clean code. The five principles are:
- The Single Responsibility Principle — Classes should have a single responsibility and thus only a single reason to change.
- The Open/Closed Principle — Classes and other entities should be open for extension but closed for modification.
- The Liskov Substitution Principle — Objects should be replaceable by their subtypes.
- The Interface Segregation Principle — Interfaces should be client specific rather than general.
- The Dependency Inversion Principle — Depend on abstractions rather than concretions.
The Dependency Inversion Principle
At its heart, the DIP is about structure. The manner in which you structure your program entities, and the way in which they interact with one another, has a direct impact on your ability to conform to the rest of the SOLID principles (the benefits of which we have discussed previously.) If your dependencies are mismanaged, then the likelihood of your code being flexible, maintainable, and reusable is drastically diminished.
In his paper on the DIP, Robert C. Martin enumerates the primary characteristics of poorly-designed software as follows: it is rigid, meaning that it is hard to change due to the cascading effects of changes in one place into another; it is fragile, meaning that changes result in unexpected breakage; and, it is immobile, in that you cannot reuse entities due to their entanglement with one another.  Martin attributes these problems primarily to poor structural design. As entities become more tightly coupled to one another, their rigidity, fragility, and immobility increase. In other words, dependency management is the key to writing software that is more flexible and therefore easier to maintain and reuse.
If the problem with bad software is related to dependency structure, then what is the solution? It is here that Martin flips the classic dependency structure on its head and argues for dependency inversion. In traditional entity layering, higher level entities depend on lower level entities, which in turn depend on even lower level entities. This is a typical top-down structure wherein entities that perform work at the policy level will delegate behavior down an increasingly detail-focused chain of dependencies. The problem with this model is that changes at the lower level can force changes at the higher level, which in turn makes reuse of higher level entities very difficult. Compare this to an “inverted” dependency structure wherein both high-level and low-level entities depend on shared abstractions. Here, different layers within a software program form a contract with one another to use a shared abstraction when they interact. In doing so, the layers free themselves to implement details however they may choose without concern for affecting one another.
Put concisely, the DIP says that high- and low-level modules should depend on mutual abstractions, and furthermore, that details should depend on abstractions rather than vice versa. By implementing a dependency structure that follows this principle, you can free your modules from one another in a way that opens them up for reuse. So long as an entity conforms to the prescribed contract of its abstraction dependencies, it can be used anywhere.
A Failure to Abstract
Of course, as with any principle that is meant to be applied to real-world work, the best way to understand it is through examples and practice. Let’s start by looking at a program that could benefit from better use of abstractions.
Here we have an perfectly reasonable first attempt at writing a program that governs the activities of a restaurant. For brevity’s sake, our restaurant can’t do much, but you can see how the program might be expanded to include other functionality. Currently, our
Restaurant class has an
Oven member and a method called
Cook, which calls the
Oven object’s three methods:
ExtinguishGas. Indeed, when this program executes we are able to successfully instantiate a
Restaurant, name it
“Bakery”, and use it to make some delicious cookies.
So, if everything in this program works, what’s the problem? Consider the following:
Restaurantclass depends on usage of its
Ovenobject. What if we wanted to make a restaurant that uses a different kind of cooking instrument? As currently implemented, we couldn’t do so without going into the Restaurant class and making changes, which would violate the Open/Closed Principle.
- Changes to the
Ovenobject have the potential to cascade through the program and break the Restaurant class’
Cookmethod. For example, what if we decided that we wanted to make our ovens electric rather than gas and changed the
ExtinguishGasmethod names? Doing so would effectively break
Restaurantbecause it relies on using those
Ovenmethods as currently named.
- The coupling between
Ovenreduces portability, meaning that we can’t re-use
Restaurantin another location without bringing
Ovenwith it. (Even if the other program never uses
The above is an example of a program that works, but is poorly designed. It is rigid, fragile, and immobile. Certainly we can do better.
Abstraction and Inversion
Our sample program leaves a lot of room for improvement. As a first step to designing a better dependency structure, we should consider what kind of abstractions actually exist here. Our
Restaurant uses an
Oven as part of the work accomplished in its
Cook method. This is a great clue — the intent actually has nothing to do with the
Oven, rather, the intent is to cook more generally. Surely our chef knows that there are other ways to cook food than with an oven, so let’s give her more choices by abstracting away the idea of a device that cooks something.
In this version of the program we created an abstraction called
ICooker, which will serve as the mutual contract between our
Restaurant and any cooking device we care to give it. Instead of hard-coding a particular cooking device into
Restaurant, we pass it one at instantiation, which it then uses in its
Cook method. On the other side of the equation, our
Oven class now implements the
ICooker interface, which lets it fill the roll of a general “cooker” device. And to prove that other devices can fill the same roll, we also have a Stove class that does the same. When this program executes, we are able to use the same
Restaurant class to instantiate two different restaurants, one with an
Oven and one with a
Stove. Both of them operate as expected and produce cookies and crepes respectively.
If you compare this version to our initial attempt, you’ll see a number of structural improvements.
Restaurantclass no longer depends on the
Ovenclass, meaning that we can create and use as many restaurants with as many different cooking instruments as we like, so long as they all abide by the contract defined in the
Ovenclass is generalized such that it implements the
Cookmethods prescribed in the
ICookerinterface. As a result, we can make changes in the
Ovenclass (for example, defining whether it is a gas or an electric oven) without affecting the Restaurant class.
- All of our classes have increased portability because they are loosely coupled. Each depends on the
ICookerinterface, but that is an abstraction that we can easily carry to other programs without having to bring implementation details with it.
Surely there is still room for improvement in this short program (perhaps implementation of an
IKitchen interface that could also be used in home kitchens and food trucks…); however, thanks to the DIP, it is a clear improvement over the first version when it comes to flexibility, maintainability, and reusability.
The final SOLID principle of software development is the Dependency Inversion Principle (DIP), which says that both high- and low-level modules should depend on mutual abstractions rather than directly on one another. Use of the DIP improves program flexibility and maintainability because it sets up dependencies in a way that decreases the effect that changes in one place have on another. Furthermore, the DIP increases the degree to which an entity from one program can be reused in another. In order to adhere to the DIP, the first step is to understand the abstractions in your program and then use them to build contracts between your entities rather than hard-code implementation details.
And that’s it for our series on the SOLID principles! I hope that you have enjoyed the discussion and perhaps even learned from it. If you have any observations or recommendations, by all means leave them in the comments section so that other readers can benefit from them. Thanks for reading!
If you would like alerts when a new article is published you can follow me here on Medium, on Twitter. Happy coding!