Advertisement
Advertisement

More Related Content

Advertisement
Advertisement

A Series of Fortunate Events - Symfony Camp Sweden 2014

  1. A Series of Fortunate Events Matthias Noback Twitter: @matthiasnoback
  2. What are events, really? Things that happen
  3. They trigger actions
  4. Just now... Attendees arrived, triggered me to turn on microphone, which triggered you to stop talking, which triggered me to start talking
  5. Events in software Events model what happened in a system
  6. Other parts of the system can respond to what happened
  7. Imperative programming Only commands doThis(); doThat(); updateSomething($something); return $something;
  8. Extracting events doThis(); // this was done doThat(); // that was done updateSomething($something) // something was updated return $something;
  9. Starting position class PostService { ... function addComment($postId, $comment) { $post = $this->fetchPost($postId); $post->addComment($comment); $this->save($post); $this->logger->info('New comment'); $this->mailer->send('New comment'); } }
  10. Starting position class PostService { function __construct( Mailer $mailer, Logger $logger ) { $this->mailer = $mailer; $this->logger = $logger; } function addComment($postId, $comment) { ... } }
  11. Making events explicit class PostService { function addComment($postId, $comment) { ... $this->newCommentAdded(); } function newCommentAdded() { $this->logger->info('New comment'); $this->mailer->send('New comment'); } }
  12. Dependency graph PPoossttSSeerrvviiccee Logger Mailer
  13. Design issues (1) I don't think the PostService should know how to use a Mailer and a Logger
  14. Design issues (2) I want to change the behavior of PostService without modifying the class itself
  15. Fix the problems By introducing events! (later)
  16. Observer pattern Notify other parts of the application when a change occurs class PostService { function newCommentAdded() { foreach ($this->observers as $observer) { $observer->notify(); } } }
  17. Observer contract Subject knows nothing about its observers, except their very simple interface interface Observer { function notify(); }
  18. Concrete observers class LoggingObserver implements Observer { function __construct(Logger $logger) { $this->logger = $logger; } function notify() { $this->logger->info('New comment'); } }
  19. Concrete observers class NotificationMailObserver implements Observer { function __construct(Mailer $mailer) { $this->mailer = $mailer; } function notify() { $this->mailer->send('New comment'); } }
  20. Configuration class PostService { function __construct(array $observers) { $this->observers = $observers; } } $postService = new PostService( array( new LoggingObserver($logger), new NotificationMailObserver($mailer) ) );
  21. Before PPoossttSSeerrvviiccee Logger Mailer
  22. After NotificationMailObserver Observer Observer LoggingObserver Logger Mailer PostService
  23. Design Principles Party
  24. Single responsibility Each class has one small, well-defined responsibility
  25. Single responsibility ● PostService: “add comments to posts” ● LoggingObserver: “write a line to the log” ● NotificationMailObserver: “send a notification mail”
  26. Single responsibility When a change is required, it can be isolated to just a small part of the application
  27. Single responsibility ● “Capitalize the comment!”: PostService ● “Use a different logger!”: LoggerObserver ● “Add a timestamp to the notification mail!”: NotificationMailObserver
  28. Dependency inversion Depend on abstractions, not on concretions
  29. Dependency inversion First PostService depended on something concrete: the Mailer, the Logger.
  30. PostService Logger Mailer
  31. Dependency inversion Now it depends on something abstract: an Observer
  32. PostService Observer Observer
  33. Dependency inversion Only the concrete observers depend on concrete things like Mailer and Logger
  34. LoggingObserver NotificationMailObserver Logger Mailer
  35. Open/closed A class should be open for extension and closed for modification
  36. Open/closed You don't need to modify the class to change its behavior
  37. PostService Observer Observer Observer
  38. Open/closed We made it closed for modification, open for extension
  39. Event data Mr. Boddy was murdered! ● By Mrs. Peacock ● In the dining room ● With a candle stick
  40. Currently missing! class LogNewCommentObserver implements Observer { function notify() { // we'd like to be more specific $this->logger->info('New comment'); } }
  41. Event object class CommentAddedEvent { public function __construct($postId, $comment) { $this->postId = $postId; $this->comment = $comment; } function comment() { return $this->comment; } function postId() { return $this->postId; } }
  42. Event object We use the event object to store the context of the event
  43. From observer... interface Observer { function notify(); }
  44. … to event handler interface CommentAddedEventHandler { function handle(CommentAddedEvent $event); }
  45. Event handlers class LoggingEventHandler implements CommentAddedEventHandler { function __construct(Logger $logger) { $this->logger = $logger; } public function handle(CommentAddedEvent $event) { $this->logger->info( 'New comment' . $event->comment() ); } }
  46. Event handlers class NotificationMailEventHandler implements CommentAddedEventHandler { function __construct(Mailer $mailer) { $this->mailer = $mailer; } public function handle(CommentAddedEvent $event) { $this->mailer->send( 'New comment: ' . $event->comment(); ); } }
  47. Configuration class PostService { function __construct(array $eventHandlers) { $this->eventHandlers = $eventHandlers; } } $postService = new PostService( array( new LoggingEventHandler($logger), new NotificationMailEventHandler($mailer) ) );
  48. Looping over event handlers class PostService { public function addComment($postId, $comment) { $this->newCommentAdded($postId, $comment); } function newCommentAdded($postId, $comment) { $event = new CommentAddedEvent( $postId, $comment ); foreach ($this->eventHandlers as $eventHandler) { $eventHandler->handle($event); } } }
  49. Introducing a Mediator Instead of talking to the event handlers Let's leave the talking to a mediator
  50. Mediators for events ● Doctrine, Zend: Event manager ● The PHP League: Event emitter ● Symfony: Event dispatcher
  51. Before PostService LoggingEventHandler::handle() NotificationMailEventHandler::handle()
  52. After PostService EventDispatcher LoggingEventHandler::handle() NotificationMailEventHandler::handle()
  53. In code class PostService { function __construct(EventDispatcherInterface $dispatcher) { $this->dispatcher = $dispatcher; } function newCommentAdded($postId, $comment) { $event = new CommentAddedEvent($postId, $comment); $this->dispatcher->dispatch( 'comment_added', $event ); } }
  54. Event class Custom event classes should extend Symfony Event class: use SymfonyComponentEventDispatcherEvent; class CommentAddedEvent extends Event { ... }
  55. Configuration use SymfonyComponentEventDispatcherEvent; $dispatcher = new EventDispatcher(); $loggingEventHandler = new LoggingEventHandler($logger); $dispatcher->addListener( 'comment_added', array($loggingEventHandler, 'handle') ); ... $postService = new PostService($dispatcher);
  56. Symfony2 ● An event dispatcher is available as the event_dispatcher service ● You can register event listeners using service tags
  57. Inject the event dispatcher # services.yml services: post_service: class: PostService arguments: [@event_dispatcher]
  58. Register your listeners # services.yml services: ... logging_event_handler: class: LoggingEventHandler arguments: [@logger] tags: - { name: kernel.event_listener event: comment_added method: handle }
  59. Events and application flow Symfony2 uses events to generate response for any given HTTP request
  60. The HttpKernel $request = Request::createFromGlobals(); // $kernel is in an instance of HttpKernelInterface $response = $kernel->handle($request); $response->send();
  61. Kernel events
  62. kernel.request ● Route matching ● Authentication
  63. kernel.controller ● Replace the controller ● Do some access checks
  64. kernel.view ● Render a template
  65. kernel.response ● Modify the response ● E.g. inject the Symfony toolbar
  66. kernel.exception ● Generate a response ● Render a nice page with the stack trace
  67. Special types of events ● Kernel events are not merely notifications ● They allow other parts of the application to step in and modify or override behavior
  68. Chain of responsibility Some sort of request Handler 1 Handler 2 Handler 3 Some sort of request Some sort of request Response
  69. Symfony example I've got an exception! What should I tell the user? Listener 1 Listener 2 Listener 3 Exception! Exception! Response
  70. Propagation class HandleExceptionListener { function onKernelException( GetResponseForExceptionEvent $event ) { $event->setResponse(new Response('Error!')); // this is the best response ever, don't let // other spoil it! $event->stopPropagation(); } }
  71. Priorities $dispatcher = new EventDispatcher(); $dispatcher->addListener( 'comment_added', array($object, $method), // priority 100 );
  72. Concerns
  73. Concern 1: Hard to understand “Click-through understanding” impossible $event = new CommentAddedEvent($postId, $comment); $this->dispatcher->dispatch( 'comment_added', $event );
  74. interface EventDispatcherInterface { function dispatch($eventName, Event $event = null); ... }
  75. Solution Use Xdebug
  76. Concern 2: Out-of-domain concepts ● “Comment” ● “PostId” ● “Add comment to post” ● “Dispatcher” (?!)
  77. We did a good thing We fixed coupling issues
  78. But this guy, Coupling, has a sister She's called Cohesion
  79. Cohesion ● Belonging together ● Concepts like “dispatcher”, “event listener”, even “event”, don't belong in your code
  80. Solutions (1) Descriptive, explicit naming: ● NotificationMailEventListener becomes SendNotificationMailWhenCommentAdded ● CommentAddedEvent becomes CommentAdded ● onCommentAdded becomes whenCommentAdded
  81. Solutions (1) This also hides implementation details!
  82. Solutions (2) Use an event dispatcher for things that are not naturally cohesive anyway
  83. Solutions (2) Use something else when an event dispatcher causes low cohesion
  84. Example: resolving the controller $event = new GetResponseEvent($request); $dispatcher->dispatch('kernel.request', $event); $controller = $request->attributes->get('_controller'); $controller = $controllerResolver->resolve($request);
  85. Concern 3: Loss of control ● You rely on event listeners to do some really important work ● How do you know if they are in place and do their job?
  86. Solution ● “Won't fix” ● You have to learn to live with it
  87. It's good
  88. exercise control Inversion of control give up control!
  89. Just like... ● A router determines the right controller ● The service container injects the right constructor arguments ● And when you die, someone will bury your body for you
  90. I admit, inversion of control can be scary
  91. But it will ● lead to better design ● require less change ● make maintenance easier
  92. PresentationFinished AskQuestionsWhenPresentationFinished SayThankYouWhenNoMoreQuestions
  93. Symfony, service definitions, kernel events http://leanpub.com/a-year-with-symfony/c/symfony-camp Pay even less ;)
  94. Class and package design principles http://leanpub.com/principles-of-php-package-design/c/symfony-camp Get a $10 discount!
  95. Design patterns ● Observer ● Mediator ● Chain of responsibility ● ... Design Patterns by “The Gang of Four”
  96. SOLID principles ● Single responsibility ● Open/closed ● Dependency inversion ● ... Agile Software Development by Robert C. Martin
  97. Images ● www.ohiseered.com/2011_11_01_archive.html ● Mrs. Peacock, Candlestick: www.cluecult.com ● Leonardo DiCaprio: screenrant.com/leonardo-dicaprio-defends-wolf-wall-street-controversy/ ● Book covers: Amazon ● Party: todesignoffsite.com/events-2/to-do-closing-party-with-love-design/ ● Russell Crowe: malinaelena.wordpress.com/2014/04/18/top-8-filme-cu-russell-crowe/
  98. What did you think? joind.in/12541 Twitter: @matthiasnoback
Advertisement