What is Dependency Injection?
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:
- What is Dependency Injection? (you are currently reading)
- Dependency Injection versus Service Locator
- Dependency Injection and passing of dependencies
- Dependency Injection and property injection
- Dependency Injection versus Lazy loading
Sign in to submit a comment