DI and passing of dependencies

10 years ago by David Grudl     edit

You know that Dependency Injection is the obvious passing of dependencies, that is, each class openly claiming its dependencies instead of covertly acquiring them somewhere. The question is how to claim them and how to pass them on.

We can use a constructor to pass dependencies:

class Foobar
{
	private $httpRequest, $router, $session;

	function __construct(HttpRequest $httpRequest, Router $router, Session $session)
	{
		$this->httpRequest = $httpRequest;
		$this->router = $router;
		$this->session = $session;
	}

}

$foobar = new Foobar($hr, $router, $session);

Or methods:

class Foobar
{
	private $httpRequest, $router, $session;

	function setHttpRequest(HttpRequest $httpRequest)
	{
		$this->httpRequest = $httpRequest;
	}

	function setRouter(Router $router)
	{
		$this->router = $router;
	}

	function setSession(Session $session)
	{
		$this->session = $session;
	}
}

$foobar = new Foobar;
$foobar->setSession($session);
$foobar->setHttpRequest($hr);
$foobar->setRouter($router);

Or directly populate individual variables:

class Foobar
{
	/** @var HttpRequest */
	public $httpRequest;

	/** @var Router */
	public $router;

	/** @var Session */
	public $session;
}

$foobar = new Foobar;
$foobar->session = $session;
$foobar->httpRequest = $hr;
$foobar->router = $router;

Which is the best solution? To keep the article from being unreasonably long, I'll refer to Passing Dependencies by Vasek Purchart, so read it, because I'll pick up where it leaves off.

So again: which solution is best? If they were equivalent, it would probably be the last one, because class code is the shortest, and shorter code minimizes the possibility of error and saves time in writing and reading. However, the solutions are not equivalent. Rather, they are diametrically opposed.

Immutability, that is, immutability

An Immutable object is an object that does not change its state from the moment it is created. You wouldn't believe how many object design problems can be solved just by making objects immutable. But that's a topic for another article.

We will almost always want object dependencies to be immutable. And in this respect, the different flavors of forwarding differ. We can change the public variables at any time and the change cannot be detected, which disqualifies them from the game entirely and I won't consider this variant any further. Not to mention the lack of type checking. (See also thinking about how property injection could be addressed.)

You might think of replacing public with private and inserting dependencies with some of the low-level tricks (like reflection), but such a circumvention of language properties doesn't belong in general DI considerations. Private variables are not part of the public API of a class and cannot be used to declare dependencies. Also, let's not hack the language unless necessary.

We could provide immutability for methods ourselves:

	function setRouter(Router $router)
	{
		if ($this->router) {
			throw new InvalidStateException('Router has already been set.');
		}
		$this->router = $router;
	}

And since a method is not a classical generic setter, i.e. it can only be called once, we cannot expect the existence of a getter and we can consider its call as mandatory, it could use a different nomenclature. For example, the prefix inject, in this case injectRouter().

Thus, we would create a convention for clarity that we pass dependencies by methods inject.

(I should point out that we are talking about a convention useful to the programmer; no DI containers were mentioned in the article. Understandably, it could be used successfully in containers, but it is absolutely crucial to realize what is cause and what is effect.)

The use of grouting methods has its pitfalls:

  • we have to ensure the invariability ourselves.
  • it is difficult to detect when all dependencies are set to initialize an object
  • we should also verify that some dependencies have not been forgotten to be set
  • the overhead code will be quite verbose and long

All these shortcomings are solved by design by injection via constructor, so it comes out as the most convenient.

(…I mean, uh, it doesn't… But we'll get to that in a moment.)

Constructor hell

The subtle problem with passing dependencies through the constructor is that we have no clue what order the parameters are listed in. I can only think of alphabetizing them (weird, huh?). If two dependencies were of the same type, then in the order of source, destination, etc.

While hinting in the IDE or automatically generated containers can help with this problem, it doesn't change the fact that a method with unclear parameters reduces the readability of the code.

As a lazy person, I don't like those machine-repeating assignments in the constructor body either. As a shortcut, one can use:

class Foobar
{
	private $httpRequest, $router, $session;

	function __construct(HttpRequest $httpRequest, Router $router, Session $session)
	{
		list($this->httpRequest, $this->router, $this->session) = func_get_args();
	}

}

But if the last dependency is optional, it could end up at Notice: Undefined offset.

I'm considering writing an RFC for PHP to use the notation:

class Foobar
{
	private $httpRequest, $router, $session;

	function __construct(HttpRequest $this->httpRequest, Router $this->router, Session $this->session)
	{
	}

}

However, these are just syntactic niceties compared to the crucial problem of inheritance.

What happens when we create offspring:

class Barbar extends Foobar
{
	private $logger;

	function __construct(HttpRequest $httpRequest, Router $router, Session $session, Logger $logger)
	{
		parent::__construct($httpRequest, $router, $session);
		$this->logger = $logger;
	}

}

As you can see, the constructor of the descendant must:

  • enumerate the parent's dependencies
  • call the parent constructor

That's okay, the parent's dependencies are also its inheritance. It's just that there is no mechanism to force the parent constructor to be called, so one of the most annoying mistakes is to omit the call parent::__construct. So the assumption that the constructor inherently enforces dependency passing is actually wrong. The constructor is easy to work around.

It is not without interest that immutability is also an appearance, since nothing prevents calling on a finished object $barbar->__construct(...) and pushing other dependencies through. So should the constructor test whether it is called a second time? Screw it, the constructor simply must not be called again. Question of convention.

But the biggest trouble comes the moment I refactor the Foobar class, which will result in a dependency change. It will be necessary to rewrite the constructors of all the children. Sure, it's logical, but in practice it can be a fatal snag. For example, if the parent is a framework class (e.g., Presenter) whose children are written by every framework user, this will effectively make it impossible to evolve, because interfering with dependencies would be a colossal BC break.

Many of the benefits of constructor injection would dissipate like steam over a pot. If it seemed that constructor calls were enforced by the language (strong and safe), while inject method calls were just a convention (forgettable), suddenly it turns out that's not entirely true.

Other possibilities

An option that partially circumvents the constructor and inheritance problem is to use the class FooDependencies mentioned in the article Dependency Injection versus Service Locator:

class FoobarDependencies
{
	function __construct(HttpRequest $httpRequest, Router $router, Session $session)
}

class Foobar
{
	function __construct(FoobarDependencies $deps)
}

class Barbar extends Foobar
{
	function __construct(FoobarDependencies $deps, Logger $logger)
	{
		parent::__construct($deps);
		$this->logger = $logger;
	}
}

When the dependencies of the parent Foobar class are changed, it doesn't necessarily break all the children because they are passed in one variable. But woe betide if they forget to pass it… Moreover, this method requires the largest amount of overhead (even an entire overhead class).

Alternatively, the dependencies of the parent Foobar class can be passed by methods and the constructor freed up for the children. The parent would then effectively be initialized after those methods are called, so the child's constructor would be called over the uninitialized object. This is not good.

What about the other way around, parent class Foobar dependencies passed by constructor and child by methods? This eliminates all the problems, except that it's hard to detect when all the dependencies are set (due to object initialization) and if they are set at all.

What if all the dependencies of the child were set by a single method inject()? That would probably solve all the complications.

However, it's still just a two-stage parent-child case. A new injection method would have to be devised for each additional child, and it would be a problem to ensure that they are called in the correct order.

So I can imagine a new clean solution using some PHP magic inside the class to save writing overhead code, elegantly expose dependencies and pass them to variables. These could be annotated with, say, an annotation @inject, but it would be an annotation intended for this internal implementation, not a hint for the DI container. This would have an effect the moment it became a more generally accepted convention, otherwise it would just be magic.

tl;dr

Passing dependencies through different paths has its pitfalls. Using methods requires a lot of code overhead. It is not a bad idea to name these methods with a prefix other than generic setters, for example one can use inject. This is because it will provide important information for the programmer, and can be used secondarily by the DI container.

If you are not using inheritance, it is usually handiest to pass dependencies through the constructor, and PHP might simplify the syntax a bit more in future versions. But if inheritance comes into play, suddenly everything is different. It turns out that a perfect general mechanism probably doesn't even exist. Maybe it would be a good idea to try to invent one, even if it uses PHP magic.


All parts:

Further reading