What Is Dependency Injection?
Understanding Dependency Inversion & Inversion of Control

Search for a command to run...
Understanding Dependency Inversion & Inversion of Control

No comments yet. Be the first to comment.
After a few days of using the 512GB MacBook Neo with TouchID, here are my impressions after some heavy, real-world use. I'll disclaim this review by saying that I consider myself a power user; on my m

2024 was a strange year for me that was a mix of trying new things, accomplishing the best work of my career, and stark transitions into unexplored territory. I think it's important to take a moment every once in a while to step back and look at one'...

What I've Been Up To Four Months Into Freelancing

Leaving My Job with No Plan — My New Career as a Freelancer

With a few glasses of wine in me and some nostalgia-fueled synthwave playing, I found myself reflecting on my past, and my history with computers. I got to thinking of my childhood, and more specifically, my personal history with computers. I love te...

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.
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:
ChromeBrowser may require us to update CaptureService and any other classes that use ChromeBrowser.ChromeBrowser instance every time we make an CaptureService. What if we already have one somewhere? This would be inefficient.CaptureService without it creating an actual ChromeBrowser. A problem with the latter could make the CaptureService impossible to test or work on.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 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:
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.
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.