What is Dependency Injection?

10 years ago by David Grudl     edit

Dependency Injection is a simple and brilliant technique that helps you write much clearer and more predictable code.

We almost always write code for others: collaborators, users of our open source libraries, or ourselves a few years older. To avoid awkward WTF moments when using it, it's a good idea to be clear. Whether in the naming of identifiers, the verbosity of error messages, or the design of class interfaces. And to clarity I would add predictability. On purpose, would you expect the call $b->hello() in this example to somehow change the state of a completely independent nearby object $a?

$a = new A;

$b = new B;
$b->hello();

That would be weird, wouldn't it? Yeah, if the two objects were somehow explicitly linked, like if we called $b->hello($a) (i.e. with argument $a) or set $b->setA($a) beforehand, then there would be a binding between the two objects and one would expect that $b could do something with $a. But without that, it would be unexpected, unsportsmanlike and confusing…

You're saying to yourself, isn't that obvious? That only a fool would write such magic code? Then take a look at the following example, which you can find in various variations in the “blog in 15 minutes with our amazing framework” series of guides:

$article = new Article;
$article->title = '10 Things You Need to Know About Losing Weight';
$article->content = 'Every year millions of people in ...';
$article->save();

Class Article represents the blog article and method save() saves it for us. Where? Probably to a database table. Really? What if it saves it to a file on disk? And if in a database, what table? What database does it actually connect to? Is it the raw or the test one? SQLite or Mongo? Under what account?

This is the same case as in the previous example, but under $a imagine an (invisible) object representing the database connection and replace $b->hello() with $article->save(). What hasn't changed is the unpredictability and incomprehensibility of the code.

We would have to look at how method save() is implemented to see where the data is stored. We'd find that it's poking around in some global variable maintaining a database connection. We'd have to dig further to see where in the code the database connection is made, and then we'd only have a picture of how everything works.

However, even if we understood how everything is interconnected, it would be a pain to intervene. For example, how do we store an article elsewhere for testing purposes? It would probably require changing some static variable. But wouldn't that break something else?

Yeah, static variables are evil. They create hidden dependencies that keep us from controlling the code. The code controls us ☹

The solution is Dependency Injection

Dependency Injection (hereafter DI) or obvious dependency passing says: take away the responsibility of classes to get the objects they need to do their job.

If you're going to write a class that requires a database to operate, don't invent inside the class body where to get it from (i.e. from any global variable, static method, singleton, registry, etc.), but ask for it in the constructor or another method. Describe the dependencies with your API. You won't have to think as much, and you'll get clear and predictable code.**

And that's it. That's the whole glorious DI.

Let's demonstrate it in practice. Let's start with an unfortunate implementation of the class Article:

class Article
{
	public $id;
	public $title;
	public $content;

	function save()
	{
		// save to the database
		// ...but where do I get the database connection?
		// GlobalDb::getInstance()->query() ?
	}
}

The author of method save() had to solve the vexing question of where to take the database connection. If he had used DI, he wouldn't have had to think about anything (and programmers like to do that), because DI gives a clear answer: if you need a database, have someone supply it. In other words: don't go looking for anything, let someone else take care of it.

class Article
{
	public $id;
	public $title;
	public $content;

	function save(Nette\Database\Connection $connection)
	{
		$connection->table('articles')->insert(array(
			'title' => $this->title,
			'content' => $this->content,
		));
	}
}

So applying DI principles just means that we passed $connection as a parameter to the method? Really? Yes. Seriously.

The use of the Article class will of course change slightly:

$article = new Article;
$article->title = ...
$article->content = ...
$article->save($connection);

Thanks to this change, it is now perfectly clear from the code that the article will be stored in a database, and which database.

Thus, the DI solution translates into a win-win situation: the author of the Article class did not have to figure out where to get the object-database, its user did not have to search where the programmer got it. The code now makes it clear that the article is stored in the database and can very easily be left to be stored in another database.

But you can come up with a number of caveats. For example, where does the variable $connection in the last example come from? DI repeats “let someone else take care of it”. The database connection will simply be supplied by whoever calls the above code.

Nojo, but now it looks like using DI will complicate the code considerably because you have to store and pass the database connection to create the instance Article. Additionally, over time, there may be a need to format some data in the Article class, and even more objects will need to be passed in accordance with DI. This would complicate our controllers, for example:

class ArticlePresenter
{
	function __construct(Connection $connection, TextFormatter $formatter, ...)
	{
		$this->connection = $connection;
		$this->formatter = $formatter;
		...
	}

	function createArticle()
	{
		return new Article($this->connection, $this->formatter, ...);
	}
}

When the presenter has to deal with other similar classes like Article, it will have a heap of dependencies. What's more, Article should undergo refactoring, where we replace the database with a more generic repository ArticleStorage, or remove the responsibility of storing itself entirely and delegate it to a new class ArticleRepository. This would mean modifying the application in many places; at least wherever instances are created Article. What to do about it?

The elegant solution is factories. Instead of manually (i.e., by operator new) creating objects Article, we have the factory make them. And instead of passing all the dependencies of a class Article, we'll just pass its factory:

class ArticleFactory
{
	function __construct(Connection $connection, TextFormatter $formatter, ...)
	{
		$this->connection = $connection;
		$this->formatter = $formatter;
		...
	}

	function create()
	{
		return new Article($this->connection, $this->formatter, ...);
	}
}

The original ArticlePresenter will not only simplify beautifully, but its API will also better capture the essence, i.e. ArticlePresenter doesn't need a database, it just wants to work with articles. Such refactoring makes one feel really good:

class ArticlePresenter
{
	function __construct(ArticleFactory $articleFactory)
	{
		$this->articleFactory = $articleFactory;
	}

	function createArticle()
	{
		return $this->articleFactory->create();
	}
}

In practice, it turns out that each class has only a few dependencies that we pass to it. Thus, consistent use of DI does not complicate the code in any way, despite concerns, and the benefits such as readability clearly outweigh the extra typing in the constructors.

It can also be argued that the non-transparent behavior of the original class Article, which stored the article somewhere unknown, doesn't really matter, as long as the method load() can retrieve it again. The joke is that we can always create a working wrapper over code designed according to the DI principle. But the reverse cannot be achieved.

Dependency Injection is a technique from the Inversion of Control (IoC) family, which includes the Service locator. It is often referred to as a kind of evil twin. We will discuss why to avoid it in the next article DI versus Service Locator.


All parts:

Further reading