Intro to dependency injection
Do you remember your first program?
We don't know what language it was written in, but if it were PHP 7, it would probably look something like this:
function addition(float $a, float $b): float
{
return $a + $b;
}
echo addition(23, 1); // prints 24 … I'd rather check it out … yeah, it will
Did we guess it right?
In fact, everything in this code is important. There are variables, code that breaks down into smaller units e.g. functions that take input parameters and return some results. Sure, conditions and cycles are missing, but they were in your second program 🙂 We will stay with the first one for now.
Passing input data to the function and returning the result is the cornerstone of programming. A perfectly understandable concept that even a beginner will understand. This concept is used not only in all other languages but also in other fields such as mathematics.
Function has its signature, which consists of name, information about input parameters and returned value, and an internal implementation. As a user we are interested in the signature, we do not need to know anything about internal implementation.
Now imagine that the function signature would look like this:
function addition(float $x): float
Sum with one parameter? That's weird… How about this?
function addition(): float
That's really weird, isn't it?
How is it used?
echo addition(); // what would it print?
Looking at such code, we would all look exactly like this.
Not only a beginner does not understand it, but also a handy programmer like you won't understand that code either.
You may be wondering why we mention such crazy examples? No one would ever
code like that! Unfortunately, this is not true. In fact, a lot of smart people
program this way. And they do it either out of ignorance or from laziness to
program better, or because others have taught them this way. Perhaps their
framework tells them that it is a good idea to write function
addition()
like this:
function addition(): float
{
$a = Input::get('a');
$b = Input::get('b');
return $a + $b;
}
That is, a function addition()
that somehow
self-obtains addends. Not only does it completely violate the idea about
functions that we have, and that it's use is incomprehensible, but also:
- creates hidden links, which we will uncover only by looking into the implementation
- it is difficult to trace where the addends come from
- and it's difficult to change them
- we have no idea what this change can affect (maybe it affects function
multiply()
too) - after all, it is not the purpose of the
addition
function to obtain it's inputs; it's responsibility is only the addition
Ufff, quickly back to the original form:
function addition(float $a, float $b): float
{
return $a + $b;
}
Beautiful. And let's make a promise that in Nette we will always write all the functions and classes exactly in this manner. Without hidden links. So that the code is comprehensible not only to the author but also to anyone who will read it after him. To make everything understandable from signatures. So we don't have to look for hidden secrets in implementation.
There is a name for this way and it's called dependency injection.
(“Dependency injection” is something completely different from “dependency injection container”. We will discuss it later.)
Dependency injection is a simple and great technique that helps you write more understandable and predictable code.
We almost always write code for others: coworkers, users of our open source
libraries or a few years older ourselves. To avoid unpleasant WTF moments when
using it, it is good to ensure clarity. In the naming of identifiers, verbosity
of error messages, or designing class interfaces. And to the “clarity” we
add “predictability”. Let's move from function addition()
to a
more real example:
$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 a blog article and it's method
save()
saves it. Where? Probably into a database table. Really?
What if it saves it to a file on disk? And if it's in the database, in which
one? In PostgreSQL or Redis? Under what user ?
We would have to look on how the method save()
is implemented to
see where the data is stored. We would most likely find that it somehow
gets a database connection. Afterwards we would need to investigate where the
database connection is created in the code, and then we would have a picture of
how everything works. Too complicated.
class Article
{
public function save(): void
{
$db = DB::connection(); // hidden dependency
$db->query(...);
}
}
And now you may be wondering: after all, it doesn't matter. It is stored in the database, it should happen, I don't have to be interested in it any more. Why are you still questioning it?
Because few moments ago we have made a promise that we will write all
functions and classes without hidden connections. Because Article repository
with hidden links to the database is just as weird as
addition()
without addends and we just don't want to admit it to
ourselves. That's why we'll fix it just as we did with addition()
.
To change the hidden dependency for a given parameter:
class Article
{
public function save(Database\Connection $db): void
{
$db->query(...);
}
}
And suddenly there are no questions.
If you write a class that requires a database to work, do not think where to get it (ie from any static method, registry, singleton, Facade, etc.), but have it passed to the constructor or another method. Admit dependencies. Admit them in the API of your class. Your code will then be clear and predictable.
Related: DI documentation in Nette.
Comments
I really love to use DI.
Will it continue? 🙂
Sign in to submit a comment