The Dependency Inversion Principle

Share this article

If I ever had to make an ad-hoc “Solomonic” decision concerning the relevance of each SOLID principle, I would just dare say that the Dependency Inversion Principle (DIP) is the most underrated of all. While some central concepts in the realm of object-oriented design are generally harder to digest at first, such as separation of concerns and implementation switching, more intuitive and untangled paradigms on the other hand are simpler, like programming to interfaces. Unfortunately, the DIP’s formal definition is surrounded by a double-edged curse/blessing that often makes programmers gloss over it, as in many cases there’s an implicit assumption that the principle is nothing but a fancy expression for the aforementioned “programming to interfaces” commandment:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend upon details. Details should depend upon abstractions.
At first blush, the above statements seem to be self-explanatory. Considering at this point no one would disagree that systems built upon a strong dependency on concrete implementations are an ominous sign of bad design, switching over a few abstractions makes perfect sense. So this would lead us right back to the start, thinking that the DIP’s main concern is all about programming to interfaces. In reality, decoupling interface from implementation is just a half-baked approach when it comes to fulfilling the principle’s requirements. The missing portion is achieving the real inversion process. Of course, the question that comes up is: inversion of what? In a traditional sense, systems were always designed to work with high-level components, be it classes or procedural routines, depending upon low-level ones (the details). For instance, a logging module could have a strong dependency on a set of concrete loggers down the chain which actually log information onto the system. It’s no surprise then this scheme would noisily ripple side effects up toward the upper layers whenever the protocol owned by the loggers is modified, even if the protocol has been abstracted away. The implementation of the DIP though helps to mitigate to some extend these ripples by making the logging module own the protocol instead, therefore inverting the overall dependency’s flow. Upon inversion, the loggers should adhere faithfully to the protocol hence changing accordingly and accommodating themselves to its fluctuations if one ever occurs further down the road. In a nutshell, this shows that the DIP is, under the hood, slightly more complex than just relying on the heaps of benefits brought by a standard interface-implementation decoupling. Yes it talks about making both high and low-level modules dependent on abstractions, but at the same time the high-level modules must have ownership of those abstractions – a subtle yet relevant detail that just can’t be overlooked so easily. As you might expect, one way that may help you more easily understand what’s actually under the umbrella of the DIP is through a few hands-on code samples. Therefore, in this article I’ll be setting up some examples so you can learn how to get the most out of this SOLID principle when developing your PHP applications.

Developing a Naive Storage Module (the missing “I” in the DIP)

There are many developers, particularly those with an aversion to the chilly waters of object-oriented PHP, who tend to see the DIP and other SOLID principles as rigid dogma that pushes hard against the language’s inherent pragmatism. I can understand such thinking, as it’s rather difficult to find pragmatic PHP examples in the wild that demonstrate the principle’s real benefits. Not that I want to play myself up as an enlightened programmer (that suit just doesn’t fit well), but it’d be useful to strive for a good cause and demonstrate from a practical standpoint how to implement the DIP in a realistic use case. To start things off, consider the implementation of a simple file storage module. The module is charged with the task of reading from and writing data to a specified target file. At a very minimalist level, the module in question could be written like this:
<?php
namespace LibraryEncoderStrategy;

class Serializer implements Serializable
{
    protected $unserializeCallback;

    public function __construct($unserializeCallback = false)  {
        $this->unserializeCallback = (boolean) $unserializeCallback;
    }

    public function getUnserializeCallback() {
        return $this->unserializeCallback;
    }
    
    public function serialize($data) {
        if (is_resource($data)) {
            throw new InvalidArgumentException(
                "PHP resources are not serializable.");
        }

        if (($data = serialize($data)) === false) {
            throw new RuntimeException(
                "Unable to serialize the supplied data.");
        }

        return $data;
    }
    
    public function unserialize($data) {
        if (!is_string($data) || empty($data)) {
            throw new InvalidArgumentException(
                "The data to be decoded must be a non-empty string.");
        }

        if ($this->unserializeCallback) {
            $callback = ini_get("unserialize_callback_func");
            if (!function_exists($callback)) {
                throw new BadFunctionCallException(
                    "The php.ini unserialize callback function is invalid.");
            }
        }

        if (($data = @unserialize($data)) === false) {
            throw new RuntimeException(
                "Unable to unserialize the supplied data."); 
        }
        
        return $data;
    }
}
<?php
namespace LibraryFile;

class FileStorage
{
    const DEFAULT_STORAGE_FILE = "default.dat";

    protected $serializer; 
    protected $file;

    public function __construct(Serializable $serializer, $file = self::DEFAULT_STORAGE_FILE) {
        $this->serializer = $serializer;
        $this->setFile($file); 
    }
    
    public function getSerializer() {
        return $this->serializer;
    }
    
    public function setFile($file) {
        if (!is_file($file) || !is_readable($file)) {
            throw new InvalidArgumentException(
                "The supplied file is not readable or writable.");
        }

        $this->file = $file;
        return $this;
    }
    
    public function getFile() {
        return $this->file;
    }
    
    public function resetFile() {
        $this->file = self::DEFAULT_STORAGE_FILE;
        return $this; 
    }
    
    public function write($data) {
        try {
           return file_put_contents($this->file, 
               $this->serializer->serialize($data));    
            
        }
        catch (Exception $e) {
            throw new Exception($e->getMessage());
        }
    }
    
    public function read()
    {
        try {
            return $this->serializer->unserialize(
                @file_get_contents($this->file));
            
        }
        catch (Exception $e) {
            throw new Exception($e->getMessage());
        }
    }
}
The module is a pretty naive structure, composed of just a few basic components. The first class reads and writes data to the file system, and the second is a simplistic PHP serializer used internally to generate a storable representation of the data. These sample components neatly do their business in isolation, and could be wired together to work in sync as follows:
<?php
use LibraryLoaderAutoloader,
    LibraryEncoderStrategySerializer,
    LibraryFileFileStorage;    

require_once __DIR__ . "/Library/Loader/Autoloader.php";
$autoloader = new Autoloader;
$autoloader->register();

$fileStorage = new FileStorage(new Serializer); 
$fileStorage->write(new stdClass());
print_r($fileStorage->read());

$fileStorage->write(array("This", "is", "a", "sample", "array"));
print_r($fileStorage->read());

$fileStorage->write("This is a sample string.");
echo $fileStorage->read();
At first glance, the module exhibits fairly decent behavior considering that its functionality allows to save and fetch a nice variety of data from the file system without much hassle. Moreover, the FileStorage class injects in the constructor a Serializable interface, thus depending on the flexibility provided by an abstraction rather than a rigid concrete implementation. With this slew of benefits shining bright on their own, what could be wrong with the module? As usual, shallow first impressions can be tricky and blurred. Looking a bit closer, not only is it clear FileStorage actually depends on the serializer, but because of this tight dependency, storing and pulling in data from the target file is constrained to using only PHP’s native serializing mechanism. What would happen if the data must be passed along to an external service as XML or JSON? The carefully-crafted module isn’t reusable anymore. Sad but true! The situation raises a few interesting points. First and foremost, FileStorage still exhibits a strong dependency on the low-level Serializer, even when the protocol that makes them interoperate with each other has been isolated from the implementation. Second, the level of genericity exposed by the protocol in question is very restrictive, limited to merely swapping out one serializer for another. Not only is depending on an abstraction a delusive perception in this case, but the real inversion process encouraged by the DIP is never achieved. It’s possible to refactor some portions of the file module to make it adhere faithfully to the DIP’s mandates. In doing so, the FileStorage
class would acquire the ownership of the protocol utilized to store and pull in file data, thus getting rid of the dependency on a lower-level serializer and affording you the ability to switch between several storage strategies at runtime. In doing so, you’d be actually gaining a lot of flexibility for free. Therefore, let’s continue along and see how to turn the file storage module into a true DIP-compliant structure.

Inverting Protocol Ownership and Decoupling Interface from Implementation (getting the most out of the DIP)

While there aren’t a plethora of options per se, nevertheless there are a few approaches that can be employed for effectively inverting the ownership of the protocol between the FileStorage class and its low-level collaborator while still keeping the protocol abstract. There’s one in particular, though, which turns out to be pretty intuitive because it relies on the natural encapsulation provided right out of the box by PHP’s namespaces. To translate this somewhat intangible concept into concrete code, the first change that should be made to the module is to define a more relaxed protocol for saving and retrieving file data, that way it is easy to manipulate it through formats other than just PHP serialization. A slim, segregated interface like the one shown below does the trick with elegance and simplicity:
<?php
namespace LibraryFile;

interface EncoderInterface
{
    public function encode($data);
    public function decode($data);
}
The existence of the EncoderInterface doesn’t seem to have a profound impact in the file module’s overall design, but it does a lot more than what it promises at face value. The first improvement is the definition of a highly-generic protocol for encoding and decoding data. The second, which is equally important as the first one, is that now the protocol’s ownership belongs to the FileStorage class since the interface lives and breaths in the class’ namespace. Simply put, we’ve managed to make still undefined low-level encoders/decoders depend on the high-level FileStorage just by writing a properly namespaced interface. In a nutshell, this is the actual inversion process that the DIP promotes behind its academic veil. Naturally, the inversion would be a clumsy halfway attempt if the FileStorage class wasn’t amended to inject an implementer of the previous interface, so here’s the refactored version:
<?php
namespace LibraryFile;

class FileStorage
{
    const DEFAULT_STORAGE_FILE = "default.dat";
    
    protected $encoder; 
    protected $file;
    
    public function __construct(EncoderInterface $encoder, $file = self::DEFAULT_STORAGE_FILE) {
        $this->encoder = $encoder;
        $this->setFile($file); 
    }
    
    public function getEncoder() {
        return $this->encoder;
    }
    
    public function setFile($file) {
        if (!is_file($file) || !is_readable($file)) {
            throw new InvalidArgumentException(
                "The supplied file is not readable or writable.");
        }
        
        $this->file = $file;
        return $this;
    }
    
    public function getFile() {
        return $this->file;
    }
    
    public function resetFile() {
        $this->file = self::DEFAULT_STORAGE_FILE;
        return $this; 
    }
    
    public function write($data) {
        try {
           return file_put_contents($this->file,
               $this->encoder->encode($data));    
            
        }
        catch (Exception $e) {
            throw new Exception($e->getMessage());
        }
    }
    
    public function read() {
        try {
            return $this->encoder->decode(
                @file_get_contents($this->file));
            
        }
        catch (Exception $e) {
            throw new Exception($e->getMessage());
        }
    }
}
With FileStorage now declaring explicitly in the constructor the ownership of the encoding/decoding protocol, the only thing left to do is to create a set of concrete low-level encoders/decoders, thus allowing you handle file data in multiple formats. The first of these components is just a refactored implementation of the PHP serializer written before:
<?php
namespace LibraryEncoderStrategy;

class Serializer implements LibraryFileEncoderInterface
{
    protected $unserializeCallback;
    
    public function __construct($unserializeCallback = false) {
        $this->unserializeCallback = (boolean) $unserializeCallback;
    }
    
    public function getUnserializeCallback() {
        return $this->unserializeCallback;
    }
    
    public function encode($data) {
        if (is_resource($data)) {
            throw new InvalidArgumentException(
                "PHP resources are not serializable.");
        }
        
        if (($data = serialize($data)) === false) {
            throw new RuntimeException(
                "Unable to serialize the supplied data.");
        }
        
        return $data;
    }
    
    public function decode($data) {
        if (!is_string($data) || empty($data)) {
            throw new InvalidArgumentException(
                "The data to be decoded must be a non-empty string.");
        }
        
        if ($this->unserializeCallback) {
            $callback = ini_get("unserialize_callback_func");
            if (!function_exists($callback)) {
                throw new BadFunctionCallException(
                    "The php.ini unserialize callback function is invalid.");
            }
        }
        
        if (($data = @unserialize($data)) === false) {
            throw new RuntimeException("Unable to unserialize the supplied data."); 
        }
        
        return $data;
    }
}
It would certainly be redundant to dissect the logic behind Serializer. Even though, it’s worth to pointing out that it not only depends now upon a more permissive encoding/decoding abstraction, but the abstraction’s ownership is explicitly exposed at the namespace level. Likewise, we could go one step further and set out to write a few more encoders so that the benefits brought by the DIP are highlighted. With that said, here’s how another additional low-level component could be written:
<?php
namespace LibraryEncoderStrategy;

class JsonEncoder implements LibraryFileEncoderInterface
{
    public function encode($data) {
        if (is_resource($data)) {
            throw new InvalidArgumentException(
                "PHP resources cannot be JSON-encoded.");
        }
        
        if (($data = json_encode($data)) === false) {
            throw new RuntimeException(
                "Unable to JSON-encode the supplied data.");
        }
        
        return $data;
    }
    
    public function decode($data) {
        if (!is_string($data) || empty($data)) {
            throw new InvalidArgumentException(
                "The data to be decoded must be a non-empty string.");
        }
        
        if (($data = json_decode($data)) === false) {
            throw new RuntimeException(
                "Unable to JSON-decode the supplied data.");
        }
        
        return $data;
    }
}
As expected, the underlying logic that’s behind extra encoders generally resembles that of the first PHP serializer, except for any obvious refinements and variants. Additionally, the components conform to the requirements imposed by the DIP, therefore adhering to the encoding/decoding protocol defined within the FileStorage namespace. With both high-level and low-level components in the file module depending upon an abstraction, and encoders exposing a clear dependency on the file storage class, we could safely claim that the module behaves as a true DIP-compliant structure. Moreover, the following example shows how to bring the components together:
<?php
use LibraryLoaderAutoloader,
    LibraryEncoderStrategyJsonEncoder,
    LibraryEncoderStrategySerializer,
    LibraryFileFileStorage;    

require_once __DIR__ . "/Library/Loader/Autoloader.php";
$autoloader = new Autoloader;
$autoloader->register();

$fileStorage = new FileStorage(new JsonEncoder); 
$fileStorage->write(new stdClass());
print_r($fileStorage->read());

$fileStorage = new FileStorage(new Serializer);
$fileStorage->write(array("This", "is", "a", "sample", "array"));
print_r($fileStorage->read());
Aside from some naive subtleties that the module exposes to client code, it’s useful for making the point and to demonstrate in a pretty instructive manner why the DIP’s predicates are actually more extensive than the old “programming to an interface” paradigm. It describes and explicitly prescribes an inversion of dependencies, and as such, it should be fulfilled through different mechanisms. PHP’s namespaces are a great way to accomplish this without excessive burden, even though traditional approaches like defining well-structured, expressive application layouts can yield the same results.

Closing Remarks

In general, opinions based on subjective expertise tend to be rather biased, and certainly the ones I expressed at the beginning of this article aren’t any exceptions. There is, however, a slight tendency to overlook the Dependency Inversion Principle in favor of its more convoluted SOLID counterparts, as it’s quite easy to misunderstand it as a synonym for dependency on abstractions. Furthermore, some programmers tend to react intuitively and think of the term “Inversion” as an abbreviated expression for Inversion of Control, which, although related to each other, is ultimately a wrong conception. Now that you know what’s really under the hood of the DIP, make sure to exploit all of benefits that it brings to the table, something that will surely make your applications a lot less vulnerable to Fragility and Rigidity issues that might eventually arise as they grow over time. Image via kentoh/ Shutterstock

Frequently Asked Questions about Dependency Inversion Principle

What is the main purpose of the Dependency Inversion Principle (DIP)?

The Dependency Inversion Principle (DIP) is a key aspect of the SOLID principles in object-oriented programming. Its main purpose is to decouple software modules. This means that higher-level modules, which provide complex logic, are separated from lower-level modules, which provide basic, fundamental operations. By doing this, changes to the lower-level modules will have minimal impact on the higher-level modules, making the overall system easier to manage and maintain.

How does DIP differ from traditional procedural programming?

Traditional procedural programming often involves higher-level modules depending on lower-level modules. This can lead to a rigid system where changes in one module can have a significant impact on others. DIP, on the other hand, inverts this dependency. Both higher-level and lower-level modules depend on abstractions, which promotes flexibility and makes the system more resilient to changes.

Can you provide a simple example of DIP in action?

Sure, let’s consider a simple example of a program that reads data from a file and processes it. In a traditional approach, the processing module might directly depend on the file reading module. However, with DIP, both modules would depend on an abstraction, such as a ‘DataReader’ interface. This means that the processing module is not directly tied to the file reading module, and we could easily switch to a different data source (like a database or a web service) without having to change the processing module.

What are the benefits of using DIP in my code?

DIP can bring several benefits to your code. It promotes decoupling, which makes your system more flexible and easier to modify. It also improves the testability of your code, as dependencies can be easily mocked or stubbed out. Furthermore, it encourages good design practices, such as programming to an interface rather than an implementation.

Are there any drawbacks or challenges with implementing DIP?

While DIP has many benefits, it can also introduce complexity, especially in large systems where the number of abstractions can become difficult to manage. It may also lead to more code being written, as you need to define interfaces and potentially create additional classes to implement those interfaces. However, these challenges can be mitigated with good design and architecture practices.

How does DIP relate to the other SOLID principles?

DIP is the last principle in the SOLID acronym, but it’s closely related to the others. For example, the Single Responsibility Principle (SRP) and the Open-Closed Principle (OCP) both promote decoupling, which is a key aspect of DIP. The Liskov Substitution Principle (LSP) and the Interface Segregation Principle (ISP) both deal with abstractions, which are central to DIP.

Can DIP be used in languages other than Java?

Absolutely. While DIP is often discussed in the context of Java and other object-oriented languages, the principle itself is language-agnostic. You can apply DIP in any language that supports abstractions, such as interfaces or abstract classes.

How can I start applying DIP in my own code?

A good starting point is to look for areas in your code where a high-level module is directly depending on a low-level module. Consider whether you can introduce an abstraction between these modules to decouple them. Remember, the goal is not to eliminate all direct dependencies, but to make sure that dependencies are on abstractions, not on concrete implementations.

Can DIP improve the performance of my code?

DIP is primarily about improving the structure and maintainability of your code, not its performance. However, by making your code more modular and easier to understand, it can help you identify and address performance bottlenecks more effectively.

Is DIP only useful in large, complex systems?

While the benefits of DIP are often more apparent in large, complex systems, it can also be useful in smaller projects. Even in a small codebase, decoupling modules can make your code easier to understand, test, and modify.

Alejandro GervasioAlejandro Gervasio
View Author

Alejandro Gervasio is a senior System Analyst from Argentina who has been involved in software development since the mid-80's. He has more than 12 years of experience in PHP development, 10 years in Java Programming, Object-Oriented Design, and most of the client-side technologies available out there.

Expert
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week
Loading form