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.
Top comments (0)