DEV Community

Naomi Dennis
Naomi Dennis

Posted on

2

Dependency Inversion Principle

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:

class Laptop{}
class DisplayPortCord{
public void connect(Laptop device){
connectedDevice = device;
}
private Laptop connectedDevice;
}
class Monitor{
public Monitor(DisplayPortCord videoConnector){
this.videoConnector = videoConnector;
}
public void connectToDevice(Laptop device){
videoConnector.connect(device);
}
private DisplayPortCord videoConnector;
}

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:

class Laptop{}
/* Added video interface */
interface VideoCable{
public void connect(Laptop device);
}
class DisplayPortCable implements VideoCable{
public void connect(Laptop device){
connectedDevice = device;
}
private Laptop connectedDevice;
}
class Monitor{
public Monitor(VideoCable videoConnector){
this.videoConnector = videoConnector;
}
public void connectToDevice(Laptop device){
videoConnector.connect(device);
}
private VideoCable videoConnector;
}

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.

/* Add device interface */
interface Device{ }
interface VideoCable{
public void connect(Device device);
}
class Laptop implements Device{}
class DisplayPortCable implements VideoCable{
public void connect(Device device){
connectedDevice = device;
}
private Device connectedDevice;
}
class Monitor{
public Monitor(VideoCable videoConnector){
this.videoConnector = videoConnector;
}
public void connectToDevice(Device device){
videoConnector.connect(device);
}
private VideoCable videoConnector;
}

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.

class Laptop; end
class DisplayPortCord
def connect(device)
self.connectedDevice = device;
end
private
attr_accessor :connectedDevice;
end
class Monitor
def Monitor( videoConnector)
self.videoConnector = videoConnector;
end
def connectToDevice(device)
videoConnector.connect(device);
end
private
attr_accessor :videoConnector;
end

The issue becomes more apparent as we add more classes.

class Monitor
def initialize(videoConnector)
self.videoConnector = videoConnector;
end
def connectToDevice(device)
if videoConnector.kind_of? DisplayPortCord
videoConnector.connect(device);
elsif videoConnector.kind_of? HdmiCord
videoConnector.connectToSomething(device)
end
end
private
attr_accessor :videoConnector;
end

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.

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more →

Top comments (0)

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more