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. [1] 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.






