In object-oriented programming, Dependecy Injection is an uprising pattern to achieve Inversion of Control. It consist in breaking the dependencies between classes and inject the collaborators of an object instead of having it find them without external aid. This collaborators are other objects which are passed to the unit in question in the constructor or by setters. There are many benefits of this technique other than code reuse, but today I will talk about this aspect.
DI is a fundamental practice that leads to testable code: Test-Driven Development forces the programmer to write code that is testable at the unit level. Unit testable code is necessarily decoupled code: if a class is totally coupled to another, the unit test became an integration test.
The hardware industry follow this pattern - integrated circuits are designed for testability. They're also very decoupled, as they can be connected on boards to build nearly anything. At the high level, the PC industry is full of standards for interchangeable parts: PCI bus, Usb and more. This decoupling is what let you change your monitor or keyboard without throwing away the pc.
Many posts have been written on Dependency Injection and I prefer to show an example here to get to the point: it is simple to write injectable classes, that can be reused later because their collaborators are wired but not soldered together. The language used here is php, but the concept is universal and any object-oriented language could be adopted. Sorry for the bad indentation but it's blogger fault (unit test it and you will see that it mangles spaces).
Let's start with this class:
Here's a component which is very coupled to the collaborator and that is not unit testable: we cannot call list() if we are not connected to the Internet and when its unit tests are run they will take a lot of time to dialogue with Gmail servers. There's more: we are testing not only our class but also the GmailService class; if one of them breaks, the test does not tell us which one is not satisfying its responsibility. If the interaction is not between two objects but five or six, the test became useless since we do not now where to search for a bug.class MailApplication
{
public function __construct()
{
$this->_service = new GmailService();
}
public function list()
{
$mails = array();
foreach ($this->_service->getMailFrom('xxx@gmail.com', 'password') as $mail) {
// .. do some work and highlist
$mails[] = $text
}
return $mails;
}
}
From the reuse point of view, imagine we have a customer that uses Yahoo and wants to adopt our beautiful application, but only if he can integrate his emails management. We write a YmailService class, but MailApplication knows only Gmail and we cannot tell it to use YMailService. There's no class reuse or extension.
According to DI principle, we should inject in MailApplication its dependency:
This way, we could subclass (mocking) GmailService and inject a stub object that returns canned results when its method list() is called. We can also extract an interface for GmailService if we want to have multiple implementations like in the Ymail example:class MailApplication
{
public function __construct(GmailService $service)
{
$this->_service = $service;
}
// ....
}
The test is now a real unit test and not an integration one. If it fails, we know who to blame: MailApplication since the other component is stubbed and returns fake results.class MailApplication
{
public function __construct(MailService $service)
{
$this->_service = $service;
}
// ....
}
class GmailService implements MailService ...
That's all very good; but how a MailApplication object is built?
This is the job of a factory:
Depending on configuration (again, injected configuration), the factory instance builds an instance of the application. Ironically, it's a factory that builds the major parts of your computer: you're certainly not expected to weld hardware components together, because it's not your job and you do not have the mandatory competence.class ApplicationFactory
{
public function getMailApplication()
{
if ($this->_config == ) {
$mailService = new GMailService(..);
} else {
$mailService = ....
}
return new MailApplication($mailService);
}
}
// in the "main" php script:
$factory = new ApplicationFactory($configuration);
$mailApplication = $factory->getMailApplication();
The simple approach needs refinement, as it leads to problems: suppose we have a CommentRepository. When you write a comment on this blog, it sends a mail.
Again, reusing is important because now we send mail with these classes but in the future we can switch to a library which provides html or attachment management, or reuse this CommentsRepository without even sending mails because another application does not require it.class Mail
{
public function __construct(MailService $service)
{
$this->_mailService = $service;
}
public function send($to)
{
$this->subject = '...' . strtoupper(...); // some work
$this->_mailService->mail($to, $this->subject, $this->text);
}
}
class CommentsRepository
{
public function sendAMail()
{
$mail = new Mail(...); // what I should pass?
$mail->send();
}
}
However, we cannot create the Mail object and pass it to the Comments in the constructor, since we don't know at the creation time (bootstrap of application, pre-Mvc dispatch, or whatever) how many Mail object it will need or if they will be used at all. Putting every dependency of Mail class in the constructor of CommentsRepository is ugly and makes CommentsRepository coupled to collaborators that it does not use.
There's more than one solution, and we will see them in the next post. But I tell you in advance that using another factory is not always the best approach.
Great article, I've been touting the benefits of DI for testing it good to see other people talk about the other wins you get when using DI.
ReplyDeleteI strongly advocates that testable code is decoupled code... Testing leads to maintainable applications.
ReplyDeleteSorry for my bad english. I would like to get updated with you new posts as I love to read your blog. Add me to your mailing list if you have any.
ReplyDeleteFeedburner (on the right bar at the top, Subscribe to feed link) offers e-mail delivery of these articles.
ReplyDelete