Better Code with IoC
How We Manage Dependencies
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?!
- Almost impossible to write unittest - Mocking isn't possible.
- Hard to extend/change behaviors
<?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.
Terms
Dependency Injection
http://martinfowler.com/articles/injection.html
"Inversion of Control is too generic a term, and thus people find it confusing..."
- External dependencies are explicitly injected into classes
- Constructor injection
- Setter injection
What's Wrong with It?
- Cumbersome
- Unnecessary burden to consumers
- Domino effect on just one single change
- Error-prone
- Dependencies are not always just one depth. They tend to be nested into multiple levels. You sometimes end up creating more than 15 objects just to get one object you need.
Pros
- Mocking is very easy
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?
- Unnecessary coupling on factory objects all over your codebase.
- Even factories depend on factories in many cases.
- Still not easy to test
- Mocking is possible, but cumbersome and hacky
- Still has domino effect on dependency changes
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 single object to rule them all
Terms
Service Locator
- An object which can resolve your services for you
- Entire app now depends on the particulars of your Service Locator
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
- Suffers from all the problems of factories, except that you now have one global and singleton object that manages all your dependencies, which is a little bit more convenient for consumers.
- Being a globally shared object, you're not guaranteed to get what you ask.
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');
}
}
Terms
Inversion of Control
http://martinfowler.com/bliki/InversionOfControl.html
- A design principle or design philosophy in which control of the program is handed to your code by a broader framework.
- "Don't call us, we'll call you."
- Most frameworks are a form of IoC
-
Examples
- Event-driven code
- MVC frameworks
- Template Method Pattern
- Strategy Pattern
- Dependency Injection
Terms
Dependency Inversion Principle
- A design principle in which high level modules do not depend on low level modules, but instead both depend on abstractions, and that these abstractions do not depend on implementation details - they are concerned with what is done, not how it is done.
-
Examples
- High level modules depend on interfaces, low level modules implement these interfaces.
- Abstract components into separate services or libraries, which depend on eachother through common interfaces.
Terms
IoC Container
- An IoC Framework which employs Dependency Injection
- A container object that exists above the classes that get created, and automatically injects dependencies through Dependency Injection.
- Typically called exactly once from the framework and is never called or references by application code.
Example
abstract class LoggerBackend {
abstract public function write($message);
}
class RedisLogger extends LoggerBackend {
public function __construct(Redis $redis) {
$this->redis = $redis;
}
public function write($message) {
$this->redis->zadd('logs', time(), $message);
}
}
class Logger {
public function __construct(LoggerBackendInterface $connector) {
$this->connector = $connector;
}
public function log($level, $message) {
$this->connector->write("$level: $message");
}
}
class Application {
public function __construct(LoggerInterface $logger) {
$this->logger = $logger;
}
public function doSomeFoo() {
$this->logger->log('INFO', 'Someone called doSomeFoo');
}
}
$container = new Container;
$container->bind('LoggerBackendInterface', 'RedisLogger');
$container->bind('LoggerInterface', 'Logger');
$app = $container->make('Application');
$app->doSomeFoo();Design Principles
- Inversion of Control (Hollywood Principle - Don't call us, we'll call you)
- Single Responsibility Principle
- Dependency Inversion Principle
- Program to an Interface
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
How do they work?
List of PHP IoC Containers
PHP-DI - http://php-di.org/
Aura.Di - http://auraphp.com/packages/Aura.Di/
Zend\Di - http://framework.zend.com/manual/current/en/modules/zend.di.introduction.html
Symfony\DedendencyInjection - http://symfony.com/doc/current/components/dependency_injection/introduction.html
deck
By dicontainerslide
deck
- 418