Getting Cozy with Dependency Injection

Error message

The spam filter installed on this site is currently unavailable. Per site policy, we are unable to accept new submissions until that problem is resolved. Please try resubmitting the form in a couple of minutes.
Piers

Tuning into some of the discussion on Drupal 8’s new additions, you may have heard about the Dependency Injection Component from the Symfony project. Its inclusion is one of the many architectural changes helping Drupal modernise its approach to code organization.

In this brief introduction, I'll endeavour to explain the concept of Dependency Injection, and see how it impacts our code, hopefully demystifying a topic which is simpler than you might think.

What is Dependency Injection?

If you're not sure what Dependency Injection is, I'm not surprised. It's a simple concept that lends itself to complicated explanations. Possibly because of the complicated and varying nomenclature being used around it and other closely linked concepts?

Let’s see the sort of definition I might be guilty of rattling off:

Dependency injection is a software design pattern that allows removing hard-coded dependencies and making it possible to change them, whether at run-time or compile-time.

- http://en.wikipedia.org/wiki/Dependency_injection

I think James Shore said it better with:

Dependency injection means giving an object its instance variables. Really. That's it.

Lets take a look at Dependency Injection in action by comparing this:

class A {
  public function GetB() 
  {
    return new B();
  }
} 

To its Dependency Injection enabled version:

class A {
  private $b;

  public function __construct(B $b)
  {
    $this->b = $b;
  }

  public function GetB() 
  {
    return $this->b;
  }
} 

That was Dependency Injection in action. In case you missed it, the Dependency Injection in that example takes place in the constructor. The A class is supplied with a dependency, rather than providing a factory for it.

Dependency Injection is simply the process of providing objects with things they want – put another way, injecting them with things things in which the depend.

It should be noted here that Dependency Injection is not limited to constructor injection as we saw in the example above. It can be expanded to include property injection ($a->b = new B()) and setter injection ($a->setB(new B())). In this article, I'll focus on constructor injection.

Why should we care?

The Dependency Injection pattern is interesting since it exposes broader issues about how we write code and structure our software. It brings into question things like:

1. Loose Coupling

The strength in which a piece of code is tied to others plays an important role in its portability. Strong coupling can mean dependencies can't be changed or swapped with equivalent signatures.

For instance, the Bar factory method illustrated below means Foo is tied to Bar:

class Foo
{
  private function getBar()
  {
    return new Bar();
  }
} 

In the next example, a restrictive signature type hint in the constructor will only accept Bars - disallowing other classes with a compatible interface:

class Foo
{
 	public function __construct(Bar $bar)
 	{
 	}
} 

2. Simplicity and Separation of Concerns (SoC)

Classes should aim to be restricted to core responsibilities. Simplicity is key. Large complex classes may indicate a bloating problem. Proper use of Dependency Injection helps us refactor code to smaller, more decoupled classes.

In the following example we can see this issue in play. The Command class has a factory (->getFileSystem()) and configuration getter (->getConfig($token)). Chances are that command knows way more about both configuration and building a FileSystem object than it should.

class Command
{
 	private function getFileSystem()
 	{
    return new FileSystem($this->getConfig('project_storage_root', $this->getConfig('env'));
 	}

 	private function getConfig($token)
 	{
    $config = new Config(__DIR__ . '/config.yml')
    return $config->get($token);
 	}
} 

We can improve this by using Dependency Injection. A FileSystem object should be given to Command. With few questions asked. Only that the supplied object conforms specified interface (FileSystemInterface).

class Command
{
 	private $fs;

 	public function __construct(FileSystemInterface $fs)
 	{
    $this->fs = $fs;
 	}

 	private function getFileSystem($token)
 	{
    return $fs;
 	}
} 

We have improved our code by simplifying it and adhering to a policy of SoC.

3. Testability of code

With simpler more decoupled code, we increase our ability to effectively test it. To test the code used in the previous example, we have no need for a functional file system. For our unit tests we may inject a mock object which implements FileSystemInterface.

For example we may deliver something like the following:

class FileSystemFake implements FileSystemInterface
{
	public function write($filename, $location) {
		// do nothing, this is only a stub.
	}
} 

Now we have an object which can be passed to Command during unit testing, without worrying about what FileSystemInterface implementations do. After all, if we are testing Command, we have no interest in complicating our tests by requiring a file system, configuration etc.

What Dependency Injection Isn't

To clarify any misconceptions we thought it might be useful discussing what Dependency Injection isn’t.

1. DI is not the same as DI Containers

The examples used so far have omitted a commonly discussed side topic: Dependency Injection Containers. These seek to provide an elegant way of handling large dependency maps. Take the following for example:

$transport = new SmtpTransport('mail.example.com', 'myUser', 'myPassword');
$messageManager = new MessageManager($transport);
$orderManager = new OrderReport('Acme\Entity\Order', 'Acme\Math\Tax', $messageManager); 

It's not hard to see how over many classes this could grow into a source of complexity. Instead of manually instantiating classes, Dependency Injection Containers provide a framework by which dependencies can be mapped, then objects built and are returned on request.

For example, the following equates to the previous example - but using a container:

$orderManager = $dependencyContainer['order.manager']; 

How is the Container made aware of the map? Typically, Dependency Injection Containers are used in conjunction with a configuration system which feeds information about objects and parameters straight into the container framework. Usually this includes an XML or YAML configuration store being processed by container building logic.

Drupal 8 is making use of Dependency Injection Containers. You can see it in action here:

<project root>/drupal8/core/lib/Drupal/Core/CoreBundle.php (github)

To be clear, Dependency Injection is not the same as Dependency Injection Containers. You may use Dependency Injection without a Container. You will gain the most benefit from Dependency Injection Containers in applications with significantly large Dependency maps.

2. Dependency Injection is not Overloading

Dependency Injection and Overloading are two very different things. This is true for both PHP’s concept of overloading and the broader coding worlds concept of overloading.

If your objective is to overwrite a specific methods in a class, Dependency Injection and DI Containers may help. Swapping an injected dependency with your own compatible implementation will be simpler so long as the surrounding architecture has been developed with good Dependency Injection principles. If you’re lucky, a configurable DI Container framework will be the icing on the cake.

3. Finally, Dependency Injection is not bullet proof

Dependency Injection does not equate to good code. Sorry, that part is still up to you. Just as it can be used as a technique for improvement, it can also be a tool of evil. Use it wisely and be mindful of the refactoring techniques and general principles mentioned.

Remember that these points should kept in mind:

  • tight coupling can be restrictive, so try to decouple where possible and reasonable;
  • keep your classes clean, lean and restricted to their own requirements;
  • unit testing is critical so make it testable.

Closing thoughts

Dependency Injection is on one hand a given, and on the other an opportunity. Remember that implementation is key. While Dependency Injection describes a stupidly simple process, it’s through careful consideration, understanding and sensible application it really shines.

I hope you've found this brief introduction useful. Good luck and happy coding!

Where to now?

Don’t stop here. The following fantastic resources and great reading fodder:

DrupalCon Portland

If you'd like to learn more about how Dependency Injection is used in Drupal 8, you can also catch us in DrupalCon Portland this May, where we'll be giving a training session on Drupal 8 and Symfony. Registration is now open; tickets are going fast, so get in quick!

Comments

Your first 'Dependency Injection enabled' snippet kind of misses the point of DI... Typo I presume?
 
 
public function GetB()
{
  return new B(); // should be $this->b
}
 

yep, typo. I've corrected it now. 

In the SoC example, the DI version of getFileSystem returns $fs. Shouldn't it return $this->fs?

With these concepts completely new to me, this post has come up as a life saver for me.
Just a question here.
If we create instances of the classes in the constructor itself (multiple or single), wont it create some extra load, in case there are methods which wont be using the instantiated object of that class. Not all methods will be requiring all the classes and their methods, so wont DI make it heavy, consuming memory which wont be used even.

Add new comment

Filtered HTML

  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.

glqxz9283 sfy39587p10 mnesdcuix7