Terry Harvey

Dependency Injection: Demystified

For any modern developer, dependency injection is an essential string to the bow. In this post, I intend to explain what it is, how to use it, and why you should use it. In this example, I’ll be using PHP, but the concept applies to any object oriented programming language.

Without Dependency Injection

In order to fully understand dependency injection, we need to take a look at what things would look like without it. Take the following example of code:

<?php

class Logger
{
    public function log($message)
    {
        $files = new FileSystem;

        if (! $files->exist('log.txt')) {
            return $files->create('log.txt', $message);
        }

        return $files->append('log.txt', $message);
    }
}

Fairly self-explanatory example. When the log() method is called, it will:

  • Check to see if the log file already exists. If it doesn’t, create it and set the contents of the file to the message we passed to the function.
  • Otherwise, simply append the given message to the existing log file.

Seems simple enough, but any seasoned developer will have noticed the flaw with this method immediately. In fact, the problem is in the very first line. Why, you ask? Let me explain…

Testing

If we were to write a unit test for this class, we wouldn’t want to touch the filesystem – we want to leave the log file how it is without tainting it with our tests. Even more so if your class is touching the database. This becomes very difficult with the above approach because there is no easy way to interchange that $files variable with another object. Hmm, we’re a bit stuck now.

Dependency Inversion

The dependency inversion principle (DIP) is the D in SOLID. The principle states that:

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • Abstractions should not depend upon details. Details should depend upon abstractions.

If you’re new to object oriented design, that probably went straight over your head, right? Let’s look at a real-world example. Taking the Logger class we had earlier, our client has asked for logs to be stored in the database instead of the filesystem. Sure, we could simply rewrite the log() method to cater for that, but that goes against everything the DIP stands for. So how would we handle this feature request elegantly? Let’s create an interface.

<?php

interface LogDriver
{
    public function log($message);
}

Think of an interface as a contract. Every class that implements an interface must provide their own implementation of the methods declared in the interface. Allow me to demonstrate. I’m going to make two new classes:

<?php

class FileLogDriver implements LogDriver
{
    public function log($message) {
        $files = new FileSystem;

        if (! $files->exist('log.txt')) {
            return $files->create('log.txt', $message);
        }

        return $files->append('log.txt', $message);
    }
}

class DatabaseLogDriver implements LogDriver
{
    public function log($message) {
        // We'll do this procedurally for the sake of simplicity.
        return prepareQuery("INSERT INTO `logs` (msg) VALUES (?)")
            ->bind($message)
            ->execute();
    }
}

So now we have two separate “drivers” with their own implementations which will handle logging in their own way. Now all we have to do is update our Logger class to accept one of these drivers in its constructor. We can then condense the log() method too.

<?php

class Logger
{
    protected $driver;

    public function __construct(LogDriver $driver)
    {
        $this->driver = $driver;
    }

    public function log($message)
    {
        return $this->driver->log($message);
    }
}

Excellent! Now that is all done, all that’s left is to instantiate these classes and tie them all together!

<?php

$databaseDriver = new DatabaseLogDriver();
$fileDriver = new FileLogDriver();

$logger = new Logger($fileDriver);

$logger->log('Hello world!');

Note that when we instantiate the Logger class, we are passing a FileLogDriver object to the constructor. Now, because both drivers adhere to a common interface, the objects are completely interchangeable. Instead of re-writing our original code to cater for that database functionality we discussed, we can simply change the parameter that’s passed in.

$logger = new Logger($databaseDriver);

We can also create additional implementations that adhere to that LogDriver interface, and that too will be interchangeable. And that is the idea behind DIP.

Also, now we have refactored our code to follow this principle, testing will become much simpler. I’ll write about testing at some point, so watch this space!

Conclusion

I hope that all made sense – if you have any feedback, suggestions, or requests, please do not hesitate to contact me!