Click here to load reader
Upload
kacper-gunia
View
2.180
Download
0
Embed Size (px)
Citation preview
The IoC Hydra
Kacper Gunia @cakper Technical Team Leader @SensioLabsUK
Symfony Certified Developer
PHPers Silesia @PHPersPL
CHAPTER 1: THE THEORY
THIS IS NOT YET ANOTHER DEPENDENCY INJECTION TALK ;)
SO WE NEED SOME CLARIFICATION
IOC !== DI
–Pico Container
“Inversion of Control (IoC) is a design pattern that addresses a component’s dependency
resolution, configuration and lifecycle. ”
–Pico Container
“It suggests that the control of those three should not be the concern of the component
itself. Thus it is inverted back.”
–Pico Container
“Dependency Injection is where components are given their dependencies through their
constructors, methods, or directly into fields.”
Inversion of Control
Dependency Injection
Events
AOP
WHY DO WE IOC?
PROS• Separation of concerns:
• dependency resolution, configuration and lifecycle
• Enforcing Single Responsibility Principle
• Easier testing
• Modular architecture, loose coupling
CONS
• Learning curve
• Code is harder do analyse/debug
• Moves complexity somewhere else (doesn’t remove)
• Need for extra tools like Containers / Dispatchers
HOW DO WE WRITE SOFTWARE WITHOUT IOC?
EXAMPLE
interface FileEraser{ public function erase($filename); }
EXAMPLEinterface Logger{ public function log($log); } class PrintingLogger implements Logger{ public function log($log) { echo $log; } }
EXAMPLE
class LocalFileEraser implements FileEraser{ public function erase($filename) { $logger = new PrintingLogger(); $logger->log("Attempt to erase file: " . $filename); unlink($filename); $logger->log("File " . $filename . " was erased.”); } }
EXAMPLE
$eraser = new LocalFileEraser(); $eraser->erase('important-passwords.txt');
HOW CAN WE FIX IT WITH DI?
EXAMPLE WITH DIclass LocalFileEraser implements FileEraser { private $logger; public function __construct(Logger $logger) { $this->logger = $logger; } public function erase($path) { $this->logger->log("Attempt to erase file: " . $path); unlink($path); $this->logger->log("File " . $path . " was erased."); } }
EXAMPLE WITH DI
$logger = new PrintingLogger(); $eraser = new LocalFileEraser($logger); $eraser->erase('important-passwords.txt');
What (is being executed)Known Unknown
Kno
wn
Unkn
own
Whe
n (i
s be
ing
exec
uted
)
Dependency Injection
Stages of loosening control(from the component point of view)
HOW CAN WE FIX IT WITH EVENTS?
EXAMPLE WITH EVENTS
interface Listener{ public function handle(Event $event); } interface Event{ }
EXAMPLE WITH EVENTSclass FileEvent implements Event{ private $path; public function __construct($path) { $this->path = $path; } public function getPath() { return $this->path; } }
EXAMPLE WITH EVENTS
class FileEraseWasInitialised extends FileEvent{ } class FileWasErased extends FileEvent{ }
EXAMPLE WITH EVENTSclass LoggingFileEventListener implements Listener{ private $logger; public function __construct(Logger $logger) { $this->logger = $logger; } public function handle(Event $event) { if ($event instanceof FileEvent) { $this->logger->log(get_class($event).' '.$event->getPath()); } }}
EXAMPLE WITH EVENTStrait Observable{ private $listeners = []; public function addListener(Listener $listener) { $this->listeners[] = $listener; } public function dispatch(Event $event) { foreach ($this->listeners as $listener) { $listener->handle($event); } } }
EXAMPLE WITH EVENTSclass LocalFileEraser implements FileEraser{ use Observable; public function erase($filename) { $this->dispatch(new FileEraseWasInitialised($filename)); unlink($filename); $this->dispatch(new FileWasErased($filename)); } }
EXAMPLE WITH EVENTS
$eraser = new LocalFileEraser();
$listener = new LoggingFileEventListener(new PrintingLogger()); $eraser->addListener($listener); $eraser->erase('important-passwords.txt');
What (is being executed)Known Unknown
Kno
wn
Unkn
own
Whe
n (i
s be
ing
exec
uted
)
Events
Stages of loosening control(from the component point of view)
HOW CAN WE FIX IT WITH AOP?
EXAMPLE WITH AOP USING DECORATOR
class LocalFileEraser implements FileEraser{ public function erase($filename) { unlink($filename); } }
EXAMPLE WITH AOP USING DECORATORclass LoggingFileEraser implements FileEraser{ private $decorated; private $logger; public function __construct(FileEraser $decorated, Logger $logger) { $this->decorated = $decorated; $this->logger = $logger; } public function erase($filename) { $this->logger->log('File erase was initialised' . $filename); $this->decorated->erase($filename); $this->logger->log('File was erased' . $filename); } }
EXAMPLE WITH AOP USING DECORATOR
$localFileEraser = new LocalFileEraser(); $logger = new PrintingLogger(); $eraser = new LoggingFileEraser($localFileEraser, $logger); $eraser->erase('important-passwords.txt');
What (is being executed)Known Unknown
Kno
wn
Unkn
own
Whe
n (i
s be
ing
exec
uted
)
AOP
Stages of loosening control(from the component point of view)
What (is being executed)Known Unknown
Kno
wn
Unkn
own
Whe
n (i
s be
ing
exec
uted
)
Dependency Injection Events
AOP
Stages of loosening control(from the component point of view)
FRAMEWORKS & LIBRARIES
A libraries provide functionality that you decide when to call.
Frameworks provide an architecture for the application and
decide when to call your code.
“DON’T CALL US, WE’LL CALL YOU”aka Hollywood Principle
Frameworks utilise IoC principles and can be seen as one of its manifestations.
CHAPTER 2: THE PRACTICE
DEPENDENCY INJECTION CONTAINERS
WHERE DIC CAN HELP
RESOLVING GRAPHS OF OBJECTS$dsn = 'mysql:host=localhost;dbname=testdb;charset=utf8'; $dbUsername = 'username'; $dbPassword = 'password'; $customerRepository = new CustomerRepository(new PDO($dsn, $dbUsername, $dbPassword)); $smtpHost = 'smtp.example.org'; $smtpPort = 25; $smtpUsername = 'username'; $smtpPassword = 'password'; $transport = new Swift_SmtpTransport($smtpHost, $smtpPort); $transport->setUsername($smtpUsername); $transport->setPassword($smtpPassword); $mailer = new Swift_Mailer($transport); $loggerName = "App"; $logger = new \Monolog\Logger($loggerName); $stream = "app.log"; $logger->pushHandler(new \Monolog\Handler\StreamHandler($stream)); $controller = new RegistrationController($customerRepository, $mailer, $logger);
RESOLVING GRAPHS OF OBJECTSuse Pimple\Container; $container = new Container(); $container['dsn'] = 'mysql:host=localhost;dbname=testdb;charset=utf8'; $container['db_username'] = 'username'; $container['dsn'] = 'password'; $container['pdo'] = function ($c) { return new PDO($c['dsn'], $c['db_username'], $c['dsn']); }; $container['customer_repository'] = function ($c) { return new CustomerRepository($c['pdo']); };
RESOLVING GRAPHS OF OBJECTS$container['smtp_host'] = 'smtp.example.org'; $container['smtp_port'] = 25; $container['smtp_username'] = 'username'; $container['smtp_password'] = 'password'; $container['transport'] = function ($c) { return new Swift_SmtpTransport($c['smtp_host'], $c['smtp_port']); }; $container->extend('transport', function ($transport, $c) { $transport->setUsername($c['smtp_username']); $transport->setPassword($c['smtp_password']); return $transport; }); $container['mailer'] = function ($c) { return new Swift_Mailer($c['transport']); };
RESOLVING GRAPHS OF OBJECTS$container['logger_name'] = "App"; $container['stream_name'] = "app_" . $container['environment'] . ".log"; $container['logger'] = function ($c) { return new \Monolog\Logger($c['logger_name']); }; $container->extend('transport', function ($logger, $c) { $logger->pushHandler( new \Monolog\Handler\StreamHandler($c[‘stream_handler']) ); return $logger; }); $container['stream_handler'] = function ($c) { return new \Monolog\Handler\StreamHandler($c['stream_name']); };
RESOLVING GRAPHS OF OBJECTS
$container['registration_controller'] = function ($c) { return new RegistrationController( $c['customer_repository'], $c['mailer'], $c[‘logger’] ); };
LIFECYCLE MANAGEMENT
$container['session_storage'] = function ($c) { return new SessionStorage('SESSION_ID'); };
LIFECYCLE MANAGEMENT
$container['session_storage'] = $container->factory(function ($c) { return new SessionStorage('SESSION_ID'); });
WHERE DIC CAN HARM
SERVICE LOCATOR
class RegistrationController { function resetPasswordAction() { $mailer = Container::instance()['mailer']; //... } }
SERVICE LOCATOR• Coupled to container
• Responsible for resolving dependencies
• Dependencies are hidden
• Hard to test
• Might be ok when modernising legacy!
SETTER INJECTION
SETTER INJECTION
• Forces to be defensive as dependencies are optional
• Dependency is not locked (mutable)
• In some cases can be replaced with events
• We can avoid it by using NullObject pattern
SETTER INJECTIONclass LocalFileEraser implements FileEraser{ private $logger; public function setLogger(Logger $logger) { $this->logger = $logger; } public function erase($filename) { if ($this->logger instanceof Logger) { $this->logger->log("Attempt to erase file: " . $filename); } unlink($filename); if ($this->logger instanceof Logger) { $this->logger->log("File " . $filename . " was deleted"); } }}
SETTER INJECTIONclass LocalFileEraser implements FileEraser{ private $logger; public function __construct(Logger $logger) { $this->logger = $logger; } public function erase($path) { $this->logger->log("Attempt to erase file: " . $path); unlink($path); $this->logger->log("File " . $path . " was erased.”); } }
SETTER INJECTION
class NullLogger implements Logger { public function log($log) { // whateva... } } $eraser = new LocalFileEraser(new NullLogger()); $eraser->erase('important-passwords.txt');
PARTIAL APPLICATION
DI WAY OF DOING THINGS
interface Logger{ public function log($log); } class PrintingLogger implements Logger{ public function log($log) { echo $log; } }
DI WAY OF DOING THINGSclass LocalFileEraser implements FileEraser{ private $logger; public function __construct(Logger $logger) { $this->logger = $logger; } public function erase($path) { $this->logger->log("Attempt to erase file: " . $path); unlink($path); $this->logger->log("File " . $path . " was deleted"); } }
DI WAY OF DOING THINGS
$logger = new PrintingLogger(); $eraser = new LocalFileEraser($logger); $eraser->erase('important-passwords.txt');
FUNCTIONAL WAY OF DOING THINGS
$erase = function (Logger $logger, $path) { $logger->log("Attempt to erase file: " . $path); unlink($path); $logger->log("File " . $path . " was deleted"); };
FUNCTIONAL WAY OF DOING THINGS
use React\Partial; $erase = function (Logger $logger, $path) { $logger->log("Attempt to erase file: " . $path); unlink($path); $logger->log("File " . $path . " was deleted"); }; $erase = Partial\bind($erase, new PrintingLogger()); $erase('important-passwords.txt');
CHAPTER 3: THE SYMFONY
SYMFONY/DEPENDENCY-INJECTION
FEATURES
• Many configurations formats
• Supports Factories/Configurators/Scopes/Decoration
• Extendable with Compiler Passes
• Supports lazy loading of services
PERFORMANCE
PERFORMANCE OF SYMFONY DIC
• Cached/Dumped to PHP code
• In debug mode it checks whether config is fresh
• During Compilation phase container is being optimised
LARGE NUMBER OF CONFIGURATION FILES
SLOWS CONTAINER BUILDER
SLOW COMPILATION
• Minimise number of bundles/config files used
• Try to avoid using extras like JMSDiExtraBundle
• Can be really painful on NFS
LARGE CONTAINERS ARE SLOW TO LOAD
AND USE A LOT OF MEMORY
LARGE CONTAINERS
• Review and remove unnecessary services/bundles etc.
• Split application into smaller ones with separate kernels
PRIVATE SERVICES
PRIVATE SERVICES
• It’s only a hint for compiler
• Minor performance gain (inlines instations)
• Private services can still be fetched (not recommended)
LAZY LOADING OF SERVICES
LAZY LOADING OF SERVICES• Used when instantiation is expensive or not needed
• i.e. event listeners
• Solutions:
• Injecting container directly
• Using proxy objects
INJECTING CONTAINER DIRECTLY
• As fast as it can be
• Couples service implementation to the container
• Makes testing harder
USING PROXY OBJECTS• Easy to use (just configuration option)
• Code and test are not affected
• Adds a bit of overhead
• especially when services are called many times
• on proxy generation
DEFINING CLASS NAMES AS PARAMETERS
DEFINING CLASS NAMES AS PARAMETERS
• Rare use case (since decoration is supported even less)
• Adds overhead
• Will be removed in Symfony 3.0
CIRCULAR REFERENCES
Security
Listener
Doctrine
CIRCULAR REFERENCE
CIRCULAR REFERENCES
• Injecting Container is just a workaround
• Using setter injection after instantiation as well
• Solving design problem is the real challenge
Doctrine
Security
Listener
TokenStorage
BROKEN CIRCULAR REFERENCE
SCOPES
MEANT TO SOLVE “THE REQUEST PROBLEM”
SCOPE DEFINES STATE OF THE APPLICATION
PROBLEMS WITH INJECTING REQUEST TO SERVICES
PROBLEMS WITH INJECTING REQUEST TO SERVICES
• Causes ScopeWideningInjectionException
• Anti-pattern - Request is a Value Object
• Which means Container was managing it’s state
• Replaced with RequestStack
App
Controller A
Sub Request
Stateful service
Sub Request
Master Request
Stateful service
Master Request
Controller B
REQUESTS MANAGED WITH SCOPES
App
Controller A
Stateless service
Request Stack
Controller B
SubRequest
MasterRequest
REQUESTS MANAGED WITH STACK
STATEFUL SERVICES
STATEFUL SERVICES
• Fetch state explicitly on per need basis (RequestStack)
• Use Prototype scope only if you have to…
PROTOTYPE SCOPE
Prototype scope
Prototype-scoped
Service Z
Stateless Service A
Prototype scope
Prototype-scoped
Service Z
Stateless Service A
Prototype scope
Prototype-scoped
Service Z
Stateless Service A
USING PROTOTYPE SCOPE WITH STRICT = TRUE
New Instances
StatefulService Z
StatefulService Z
StatefulService Z
Stateless Service A
USING PROTOTYPE SCOPE WITH STRICT = FALSE
FORGET ABOUT SCOPES ANYWAY
WILL BE REMOVED IN SYMFONY 3.0
USE SHARED=FALSE INSTEAD
CONTROLLERS
EXTENDING BASE CONTROLLER
• Easy to use by newcomers / low learning curve
• Limits inheritance
• Encourages using DIC as Service Locator
• Hard unit testing
CONTAINER AWARE INTERFACE
• Controller is still coupled to framework
• Lack of convenience methods
• Encourages using DIC as Service Locator
• Testing is still hard
CONTROLLER AS A SERVICE• Requires additional configuration
• Lack of convenience methods
• Full possibility to inject only relevant dependencies
• Unit testing is easy
• Enables Framework-agnostic controllers
NONE OF THE ABOVE OPTIONS WILL FORCE YOU TO WRITE GOOD/BAD CODE
SYMFONY/EVENT-DISPATCHER
FEATURES
• Implementation of Mediator pattern
• Allows for many-to-many relationships between objects
• Makes your projects extensible
• Supports priorities/stopping event flow
EVENT DISPATCHER
• Can be (really) hard to debug
• Priorities of events / managing event flow
• Events can be mutable - indirect coupling
• Hard to test
INDIRECT COUPLING PROBLEM
• Two services listening on kernel.request event:
• Priority 16 - GeoIP detector - sets country code
• Priority 8 - Locale detector - uses country code and user agent
INDIRECT COUPLING PROBLEM
• Both events indirectly coupled (via Request->attributes)
• Configuration change will change the app logic
• In reality we always want to call one after another
Listener A
Listener B
Listener C
DispatcherService
INDIRECT COUPLING PROBLEM
WHEN TO USE EVENT DISPATCHER
• Need to extend Framework or other Bundle
• Building reusable Bundle & need to add extension points
• Consider using separate dispatcher for Domain events
CHAPTER 4: THE END ;)
BE PRAGMATIC
BE EXPLICIT & DON’T RELY ON MAGIC
Kacper Gunia @cakper Technical Team Leader @SensioLabsUK
Symfony Certified Developer
PHPers Silesia @PHPersPL
Thanks!https://joind.in/14979
REFERENCES• http://martinfowler.com/bliki/InversionOfControl.html
• http://picocontainer.com/introduction.html
• http://www.infoq.com/presentations/8-lines-code-refactoring
• http://richardmiller.co.uk/2014/03/12/avoiding-setter-injection/
• http://art-of-software.blogspot.co.uk/2013/02/cztery-smaki-odwracania-i-utraty.html