Moon Kyong & Gabriel Maybrun
Demand Media, 2015-02-11
<?php
class FooClass
{
public function __construct() {
$this->logger = new Logger;
}
public function doFoo() {
$this->logger->log("I did some foo");
}
}The old way was `new`
<?php
class Logger
{
public function __construct() {
$this->dataStore = new MySqlLoggerDataStore();
}
public function log($message) {
$this->dataStore->log($message);
}
}Creating objects with the new operator.
What's wrong with it?
Dependency Injection (DI)
http://martinfowler.com/articles/injection.html
"Inversion of Control is too generic a term, and thus people find it confusing..."
<?php
class aClass
{
private $logger;
public function __construct(Logger $logger)
{
$this->logger = $logger;
}
public function doSomething()
{
$this->logger->log("constructor! constructor! constructor!");
}
}
// Client Code
new aClass(new Logger(new MySqlLogger));Dependency Injection
Let the clients pass all the dependencies by hand.
What's Wrong with It?
Pros
Factory
<?php
class LoggerFactory
{
public function createMySqlLogger()
{
return new Logger(new MySqlLogDataStore);
}
}<?php
class aClass
{
public function doSomething()
{
$logger = (new LoggerFactory())->makeMySqlLogger();
$logger->log("factory! factory! factory!");
}
}
Creating an object and its dependencies through a 3rd party object that knows about dependencies
What's Wrong with It?
Service Locator
class ServiceLocator
{
private $services;
public function register($name, $callable)
{
$this->services[$name] = $callable;
}
public function get($name)
{
return $this->services[$name]();
}
}
$serviceLocator = new ServiceLocator();
$serviceLocator->register('logger', function () {
return new Logger(new MySqlLogDataStore());
});
$serviceLocator->register('UserModel', function() {
return new Model('users', new MySqlDBConnection('username', 'password');
});In bootstrap process
One object to rule them all
Service Location with Service Locator
class aClass
{
private $serviceLocator;
public function __constructor(ServiceLocator $serviceLocator) {
$this->serviceLocator = $serviceLocator;
}
public function doSomething() {
$logger = $this->serviceLocator->get('Logger');
$logger->log('locate! locate!');
}
}
class anotherClass
{
private $serviceLocator;
public function __constructor(ServiceLocator $serviceLocator) {
$this->serviceLocator = $serviceLocator;
}
public function save() {
$user = $this->serviceLocator->get('UserModel');
$user->name = 'name';
$user->save();
}
}
Pros/Cons
SL::register('Users', function() {
return new UserRepository(
SL::get('DatabaseConnection') // SQL
);
});You bind a service somewhere in the application
Someone else bind a service somewhere in the application after your binding
SL::register('Users', function() {
return new UserSearch(
SL::get('SearchBackend') // Solr
);
});class aClass
{
public function doSomething()
{
// What am I getting?
$users = SL::get('Users');
}
}
Inversion of Control
http://martinfowler.com/bliki/InversionOfControl.html
Dependency Inversion Principle
Traditional Model
IoC Container
interface IDataStream {}
interface IEmailSender {}
interface IConfigurationReader {}
interface ILogger {}
interface IEmailCredentialsProvider {}
interface IEmailSettingsProvider {}
class SomeDataStream implements IDataStream {}
class EmailSender implements IEmailSender {
public function __construct(
IEmailCredentialsProvider $credentials,
IEmailSettingsProvider $settings
) {
// Do stuff
}
}
class EmailCredentialsProvider implements IEmailCredentialsProvider {}
class EmailSettingsProvider implements IEmailSettingsProvider {}
class ConfigurationReader implements ConfigurationReader {}
class FileSystemLogger implements ILogger {}
class NotificationEngine {
public function __construct(
IDataStream $stream,
IEmailSender $emailer,
IConfigurationReader $config,
ILogger $logger
) {
// Do stuff
}
}
class NotificationEngine {
public function __construct(
IDataStream $stream,
IEmailSender $emailer,
IConfigurationReader $config,
ILogger $logger
) {
// Do stuff
}
}
// Framework Code
$container = new Container;
// Run service providers
$container->bind('ILogger', 'FileSystemLogger');
$container->bind('IDataStream', 'SomeDataStream');
$container->bind('IConfigurationReader', 'ConfigurationReader');
$container->bind('ILogger', 'FileSystemLogger');
$container->bind('IEmailCredentialsProvider', 'EmailCredentialsProvider');
$container->bind('IEmailSettingsProvider', 'EmailSettingsProvider');
// Composition Root, creates Dependency Graph
$container->make('NotificationEngine');
Composition Root
Where does it happen?
"■ A console application is an executable (.exe) with a Main method.
■ An ASP.NET web application is a library (.dll) with an Application_Start event
handler in its Global.asax.
■ A WPF application is an executable (.exe) with an App.xaml file." (Dependency Injection in .NET, 2011, p.77)
The Composition Root is the single place in your application where you use an IoC container to compose a dependency graph of a class by directly talking to the container, decoupling all the other objects in your application from the container.
A possible Composition Root in an MVC framework would be a route, request class, or controller
IoC Containers Enable Clean Code
Do I Inject Everything?
Objects without any side effects don't have to be injected.
Value Objects
class EmailAddress
{
private $emailAddress;
public function __construct($emailAddress)
{
$this->assertEmailAddress($emailAddress);
$this->emailAddress = $emailAddress;
}
private function assertEmailAddress($emailAddress)
{
if (!filter_var($emailAddress, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException(
"{$emailAddress} is in an invalid email format."
);
}
}
public function __toString()
{
return $this->emailAddress;
}
}
Do I Inject Everything?
Objects without any side effects don't have to be injected.
class aClass
{
private $repository;
public function __construct(UserRepository $repository)
{
$this->repository = $repository;
}
public function createUser($email)
{
$user = new User(new Email($this->email));
$this->repository->persist($user);
}
}
Using Value Object
IoC Containers
What's Next? S.O.L.I.D.
There ain't no such thing as a free lunch.
Writing code in a S.O.L.I.D. way creates classes with a single responsibility, high testability and reusability in an abstracted form through interfaces. It sounds good, but it also means that we create extra classes than writing code without S.O.L.I.D. in mind and we now have to manage those extra classes. That's when an IoC container shines, taking the "D" in S.O.L.I.D. IoC containers handle abstractions and dependencies we create when writing a good S.O.L.I.D. codebase.
S: Single responsibility principle
O: Open/closed principle
L: Liskov substitution principle
I: Interface segregation principle
D: Dependency inversion principle