JD Lien
WebArtisan.info

WebArtisan.info

What Is Dependency Injection?

What Is Dependency Injection?

Understanding Dependency Inversion & Inversion of Control

JD Lien's photo
JD Lien
·Aug 19, 2022·

6 min read

When you start working with frameworks like Laravel or .NET, a concept you frequently come across is "dependency injection". What dependency injection amounts to is that when one thing (like a class) needs something else, like an object, it gets it.

But what does this mean? How do we use it, and how does it work? Why is this even something we would care about? As I started learning MVC frameworks like Laravel, it took me a while to understand these concepts. This article gives a brief introduction to these concepts to help answer these questions.

What Is Meant by 'Dependency'?

Let's look at a real open source Laravel app called Screenly by Stefan Zweifel. It has a CaptureService class that takes screenshots. CaptureService itself needs, or 'depends on', a ChromeBrowser object. In this case, we say that ChromeBrowser is a dependency of CaptureService.

The first way we may think to handle this situation would be to create a new instance of the ChromeBrowser dependency on our own within the CaptureService when needed, like so:

<?php

class CaptureService
{
    /**
     * @var Screeenly\Services\Browser
     */
    protected $browser;

    public function __construct()
    {
        $this->browser = new ChromeBrowser();
    }
}

This approach does not use dependency injection. What's wrong with that?

This creates what we call "tightly coupled" classes, because the CaptureService has to handle using and creating instances of ChromeBrowser. There are a few problems with this:

  • Adding new features or changing ChromeBrowser may require us to update CaptureService and any other classes that use ChromeBrowser.
  • We have to create a new ChromeBrowser instance every time we make an CaptureService. What if we already have one somewhere? This would be inefficient.
  • Creating automated tests is difficult because we can't test CaptureService without it creating an actual ChromeBrowser. A problem with the latter could make the CaptureService impossible to test or work on.
  • What if we later want another kind of browser, like a "FirefoxBrowser"?

Such tight coupling creates maintenance headaches when we later are required to make changes to CaptureService for updates to ChromeBrowser. Instead, we would prefer our classes to be 'loosely coupled'.

A technique we use to achieve this is dependency injection. Here's what it looks like to rewrite the above example with the use of dependency injection (which is how the real project works):

<?php

use Screeenly\Contracts\CanCaptureScreenshot;

class CaptureService
{
    /**
     * @var Screeenly\Services\Browser
     */
    protected $browser;

    public function __construct(CanCaptureScreenshot $browser)
    {
        $this->browser = $browser;
    }
}

Above, we type-hinted the $browser with the CanCaptureScreenshot interface (not a real class type) and set it as a parameter for the constructor. This is enough to tell the framework to create an instance of the type of browser we want, such as ChromeBrowser.

Laravel's service container contains the smarts (via a PHP feature called reflection) to understand how to create an instance of ChromeBrowser with just this information. An instance of a class that fulfills this CanCaptureScreenshot contract (that is, a browser) is then automatically resolved and 'injected' into the __construct constructor method.

Dependency Inversion

Dependency injection is a key way we use the concept of "Dependency Inversion" (the D in the "SOLID" programming principles).

The key points of dependency inversion are these:

  • High-level modules should not depend on low-level modules
  • Both should depend on abstractions (interfaces)
  • Abstractions should not depend on details

Going back to our Screenly example, note that $browser is dynamic, as it is type hinted as the CanCaptureScreenshot interface which requires the capture method:

<?php

namespace Screeenly\Contracts;

use Screeenly\Entities\Url;

interface CanCaptureScreenshot
{
    public function capture(Url $url, $storageUrl);
}

There are multiple classes that implement the CanCaptureScreenshot interface: ChromeBrowser and InMemoryBrowser. So by type-hinting the interface, we can actually potentially use either of these classes within the CaptureService. Other browsers could potentially be added in the future that use this interface.

Laravel knows how to implement this interface, and what class to create because a service provider is configured that tells Laravel about this. See the following ScreenlyServiceProvider class

<?php

class ScreeenlyServiceProvider extends ServiceProvider
{

    public function boot()
    {
        $this->app['view']->addNamespace('screeenly', base_path().'/modules/Screeenly/Resources/views');

        // Note that this binds ChromeBrowser to the CanCaptureScreenshot Interface
        $this->app->bind(CanCaptureScreenshot::class, ChromeBrowser::class);

        auth()->extend('screeenly-token', function ($app, $name, array $config) {
            return new ScreeenlyTokenGuard(
                auth()->createUserProvider($config['provider']),
                $this->app['request']
            );
        });
    }

    public function register()
    {
    }
}

We can also observe that, for purposes of testing, we don't necessarily want to use ChromeBrowser so we can see that in the API test suite, this same interface is resolved with a different class, the InMemoryBrowser class.

<?php

class ApiV2ScreenshotTest extends BrowserKitTestCase
{
    /** @test */
    public function it_returns_base64_representation_of_screenshot()
    {
        Storage::fake(config('screeenly.filesystem_disk'));

        Storage::disk(config('screeenly.filesystem_disk'))->put('test-screenshot.jpg', file_get_contents(storage_path('testing/test-screenshot.jpg')));

        $apiKey = ApiKey::factory()->create();

        // Note that for testing we bind a *different* class to the CanCaptureScreenshot interface
        $this->app->bind(CanCaptureScreenshot::class, function ($app) {
            return new InMemoryBrowser('http://foo.bar', '/path/to/storage');
        });

        $this->json('POST', '/api/v2/screenshot', [
            'key' => $apiKey->key,
            'url' => 'http://google.com',
        ])
        ->seeJsonStructure([
            'data' => [
                'path', 'base64',
            ],
        ]);
    }
}

The above demonstrates that by using an interface, we have the flexibility to free a class from needing to know anything at all about how that interface is implemented, and this allows for easier testing and more maintainable code.

Inversion of Control

Dependency injection is a specific implementation of a more general concept called inversion of control (IoC), which has to do with whether it is us (via our own code) or the framework that decides when to create classes, run methods, and such.

In procedural code, the code works through a series of operations in order, and the code fully controls when functions are called. This is inflexible and trickier to maintain as you end up with big chunks of tightly coupled code.

A benefit of a framework like Laravel, (or also a GUI framework like for desktop apps) is that the framework can decide when and how to create classes and run the functions (or methods) contained within them when needed. This is the primary job of the service container. The control is "inverted" to the framework, which allows your code to focus on the important responsibilities, making your classes more focused and easier to maintain.

It's like if you had a chauffeur drive you to work, rather than you operating every aspect of your car yourself. It frees your mind for something you'd rather focus on.

IoC is one way in which software is made more modular and extensible. Laravel makes it so easy to use dependency injection just by type hinting variables passed into class methods which will either create the objects or use existing "singleton objects" (of which there can be only one).

I hope this introduction to the concepts of dependency injection and inversion of control has been helpful. Feel free to ask questions in the comments. And if you enjoy this kind of content, you can follow me on Twitter at @jdlien.

 
Share this