You may have heard the term "Dependency Injection", You may also have heard that "Dependency Injection" is a 25-dollar term for a 5-cent concept. What that scary name actually means is that you give your object dependencies as arguments instead of hardcoding them into your class. This is simple, yet very powerful and has many benefits which I will mention later in this article.
There are many ways to achieve Dependency Injection. Here, I will focus on "Constructor Injection" as it is the most frequently used and straightforward method, just be aware that there are other methods too.
But first things first, what is a "dependency" someone might wonder?
"Dependency" is yet another fancy name to describe that an object uses something external to perform a task. For example, when object A uses object B, the latter is called “a dependency” for the first one.
Considering the following example, I have the OrderManager class which uses a Mailer object to send emails. This means that the OrderManager has a dependency on the Mailer.
class OrderManager {
public function complete(Order $order) {
// do stuff ..
$mailer = new Mailer();
$mailer->sendMessage($order->getUser());
// do stuff ..
}
}
$orderManager = new OrderManager();
$orderManager->complete($order);
The Mailer in the above example is a hardcoded dependency. It is called hardcoded because it is instantiated inside the complete method and cannot be changed without actually editing the OrderManager class.
To slightly improve my code I can instantiate the Mailer in the constructor of the OrderManager class and assign it to a property. This will make the Mailer accessible from other methods of the OrderManager class but it is still considered hardcoded since it still cannot be changed without editing the OrderManager class:
class OrderManager {
private $mailer;
public function __construct()
{
$this->mailer = new Mailer();
}
public function complete(Order $order) {
// do stuff ..
$this->mailer->sendMessage($order->getUser());
// do stuff ..
}
public function cancel(Order $order) {
// do stuff ..
$this->mailer->sendMessage($order->getUser());
// do stuff ..
}
}
It might not seem too bad in a single instance, but having hardcoded dependencies all over the place will result in "tight coupling", a state where the classes are highly dependent on each other and changes require a lot of effort. And as we all know:
Change is the only constant in a programmer’s life.
It is also worth mentioning that everywhere a dependency is hardcoded, a new instance will be created. For example, if Mailer is hardcoded in 10 classes which will be instantiated, there will be 10 different instances of the Mailer, which will add a pointless load to the system causing a negative impact on the performance.
Another area that is negatively affected by hardcoded code, are the unit tests. When you write a unit test you often need to mock your class dependencies and this is not possible when you have hardcoded dependencies.
By simply passing the dependencies as constructor arguments, I move the instantiation responsibility away from the OrderManager class. That automatically means that I can inject the same $mailer object in other classes. Performance, check!
class OrderManager {
private $mailer;
public function __construct(Mailer $mailer)
{
$this->mailer = $mailer;
}
public function complete(Order $order) {
// do stuff ..
$this->mailer->sendMessage($order->getUser());
// do stuff ..
}
}
To further decouple, it is a good idea to turn the Mailer class into an interface. The generic name does not indicate any implementation and it is ideal for an interface that enforces a sendMessage method. I can use the Mailer type-hint in the OrderManager constructor argument to assure that the $mailer dependency has a sendMessage method.
interface Mailer
{
public function sendMessage($user);
}
By programming to an interface, it is possible to inject into the OrderManager any object that implements the Mailer interface. It could be the SmtpMailer for SMTP, the FakeMailer for testing, or the GoogleMailer for Gmail integration:
class SmtpMailer implements Mailer
{
public function sendMessage($user) {
// send email message with SMTP.
}
}
class FakeMailer implements Mailer
{
public function sendMessage($user) {
// do nothing.
}
}
And the OrderManager:
class OrderManager
{
private $mailer;
public function __construct(Mailer $mailer)
{
$this->mailer = $mailer;
}
public function complete(Order $order) {
// do stuff ..
$this->mailer->sendMessage($order->getUser());
// do stuff ..
}
}
$mailer = new SmtpMailer();
$orderManager = new OrderManager($mailer);
The fact that the OrderManager does not depend on a single concrete class and I have the flexibility to change the dependency is also known as "loose coupling".
By changing the dependency I can change the behavior inside OrderManager without changing the actual OrderManager class, this implementation also complies with the open-close principle.
Although dependency injection does not require any tooling, manually instantiating objects and their dependencies, and the dependencies of the dependencies may end up requiring a lot of work. A dependency injection container can be really helpful with that particular task. But that is a topic for another time.
In conclusion, Dependency Injection is a simple technique that makes mocking possible, has performance benefits but most importantly improves the code flexibility dramatically.