DEV Community


Dependency Inversion Principle

naomidennis profile image Naomi Dennis ・4 min read

The last SOLID rule is the dependency inversion principle. According to Robert Martin's Agile Software Development: Principles, Patterns and Practices, the principle is defined as,

1) High level modules shall not depend on low-level modules. Both shall depend on abstractions.

2) Abstractions shall not depend on details. Details shall depend on abstraction

This is a lot, so let's look at each rule separately.

High Level Modules Shall not Depend on Low-Level Modules

Let's look at a very simple Java example:

In this example, Monitor is dependent on DisplayPortCable and DisplayPortCable is dependent on Laptop. If Monitor wants to use a different cord, say an HDMI or VGA cord, we will be forced to change Monitor directly. We could give Monitor multiple constructors for each cord type; we could also create a base class that holds the behavior for all the cords. However, these approaches aren't desirable as it leaves the class rigid and cumbersome to expand and it could become bloated. A base class may seem like a good idea, but it's too tempting to add behavior for all the cords in one place and simply extend it to the newly created classes. This bloat tends to break the interface segregation principle. The same holds true for the relationship between DisplayPortCable and Laptop.

In statically typed languages like Java, it's best to invert the dependency using an interface (or virtual classes in C++). Interfaces allows us to define a low level type for our higher level classes (like Monitor) to use.

Inverting a Dependency

When inverting a dependency, we want the classes that are being depended on (in our example DisplayPortCable and Laptop) to instead, depend on an interface. Continuing with our example, the relationship between DisplayPortCable and Monitor looks like so:

From the example, we create an interface for VideoCable and implement the cable we plan on using for Monitor. Instead of Monitor being restricted to accepting DisplayPortCable it can now accept any VideoCable.

There is still a dependency in our code because VideoCable is dependent on Laptop. This helps brings us to the next part of this principle.

Abstractions Shall not Depend on Details. Details Shall Depend on Abstraction

When thinking about the details of a class, I like to think that details mean the behavior and properties of a class. In our example, this means #connect() and #connectedDevice. Although our DisplayPortCable is abstracted, it's still dependent on Laptop. The question we ask is the same that we asked before. What if a VideoCable wanted to connect to a Desktop or a Smartphone instead of a Laptop?

And so, we invert the dependency like we did before.

Dynamic vs Static Approaches to Dependency Inversion

The example above was done in Java. Statically typed languages usually have a concrete way of defining abstract interfaces. However, in dynamically typed languages, the idea of an abstract class is harder to implement as the tools to do so are not typically a feature of the language. In cases like this, we can use a technique known as duck typing.

Duck typing is the idea, that if the behavior of a class walks in a particular way, and talks in a particular way, that way can be abstracted to the dependent classes.

The difference between an interface-esque keyword approach to dependency inversion and duck typing, is the former is written in contract form where the behavior is explicitly defined and enforced; the latter is not explicitly defined and becomes apparent as more object types are used within a class. If we were to convert our above code to Ruby, duck typing wouldn't be obvious because there's only one object type depending on an abstraction.

The issue becomes more apparent as we add more classes.

Since there isn't a defined contract, we could use any old interface with our new classes and simply ask for the type of videoConnector and use whatever interface was defined. However, this not only makes Monitor difficult to extend (since we'd have to manually add a new conditional to understand a new type), but it leads Monitor to know about the inner workings of other classes which directly violates this principle. Monitor has to depend on an abstraction of the object it's receiving. Monitor shouldn't care about the object's class, just its behavior.

To do this, we have to consciously ensure the classes have an agreed upon interface and that they depend on this interface. In the case of our example, we have to dictate if #connectToDevice() will expect videoConnector to have the #connect() or #connectToSomething() behavior.

In Conclusion

Dependency inversion can be tricky. However, it allows a class to be more flexible and trains us to think about classes in terms of behavior, rather than construction.

Discussion (0)

Editor guide