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.
Let's look at a very simple Java example:
In this example,
Monitor is dependent on
DisplayPortCable is dependent on
Monitor wants to use a different cord, say an
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
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.
When inverting a dependency, we want the classes that are being depended on (in our example
Laptop) to instead, depend on an interface. Continuing with our example, the relationship between
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
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.
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
#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
And so, we invert the dependency like we did before.
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
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.