Your might have heard of phrases like Dependency Injection, Constructor Injection and Setter Injection. These sound a bit confusing at first, but at the end, the concepts are very simple, but also very powerful.
Dependency Injection
First, let me explain the phrase Dependency Injection, because it's maybe the simplest one and you most likely already use it every day. Let's say we have a Validator
class which somehow validates things. Now we also have a Translator
class, which translates things somehow. As we know that separation of concerns is a good thing, they are decoupled - the Validator
does know nothing about translating things and the Translator
does know nothing about validating things. But we might also want to translate the things that were validated. So the Validator
needs some help from the Translator
- it depends on it or it has a dependency. To give the Validator
access to the Translator
we pass it in - or inject it. We inject a dependency. That's all what dependency injection is, passing objects to other objects.
Setter Injection
Now we have several ways to achieve this. We could simply pass it every time something is validated, for example $validator->validate($someObject, $translator)
, but this clutters the validate
-method and is not very intuitive. Instead, we could store the dependency as a property of the Validator
. Now to pass it in we simply use a setter like $validator->setTranslator($translator)
. After we have done this we can call the validate
-method as often as we like and it will always use the passed Translator
. The Validator
now looks something like this:
class Validator
{
private $translator;
public function setTranslator($translator)
{
$this->translator = $translator;
}
public function validate($someObject)
{
// do the magic here with the help of $this->translator
}
}
That is all that setter injection is. Now this is great for optional dependencies. In our example the validate
-method should also work fine with no Translator
set because there is no guarantee it has been set before the validate
-method has been called!
But what if we want to enforce that?
Constructor Injection
If you have a dependency that you cannot leave away because your class does not work without it, constructor injection is the way to go. Instead of calling a set method, the dependency is passed via the constructor. Now there is no way around, but passing it when an object is created. This means for our example that we have to pass a Translator
to the Validator
in its constructor. This will look something like this:
class Validator
{
private $translator;
public function __construct($translator)
{
$this->translator = $translator;
}
public function validate($someObject)
{
// do the magic here with the help of $this->translator
}
}
Constructor Injection is great for obligatory dependencies. You clearly communicate to the user of that class: You have to pass me such an object, otherwise, I won't work.
So what should I use?
It absolutely depends on the use case. While in many situations a class simply does not work without its dependencies, it's perfectly fine to leave them away in other. For example, your Validator
might not work without a Translator
, because you always want to display the errors to your users. On the other hand it maybe also has a dependency on a Logger
class which is able to write the errors to a log file or send it via email. But this might be completely optional.
Interfaces
At the beginning, I said that the Validator
should know nothing about how to translate things. But now we have a hard dependency to the Translator
. But what if we want to use a different implementation at some point? This might be more obvious with the example of a Logger
, so let's talk about that one. So our Validator
is also able to log its results to a log file. We used setter injection here to pass in a FileLogger
:
class Validator
{
private $logger;
public function setLogger(FileLogger $logger)
{
$this->logger = $logger;
}
}
That's fine - until we decide that we need to be informed about the results via email instead of having to look through the log files manually. Now we need to edit the Validator
-class to pass in an EmailLogger
instead. That's a bad thing because editing existing code always has the risk to break something. And after all, we just want to change the way we log things - why do we need to change a Validator
for that?
Interfaces to the rescue: Instead of type hinting for the concrete implementation of the FileLogger
in our Validator
-class we type hint for the interface Logger
. Now our FileLogger
and EmailLogger
simply implement that interface and we are free to change the logger we pass as often as we want - without changing a single line of the Validator
code.