Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Better Living Through Events

Better Living Through Events

Effectively decoupling code can be difficult: the user clicks a button and 12 things need to happen, but you don't want those things all tied to your button code. Events to the rescue! In this talk, Bill will discuss events, what they are, patterns for using them, some libraries out there to help you write event-driven code.

8145132ebae0c1f62cdd7b6126d71768?s=128

Bill Israel

July 09, 2013
Tweet

More Decks by Bill Israel

Other Decks in Technology

Transcript

  1. Better Living Through Events order.complete sendEmail() updateReport() drinkBeer() July 9,

    2013 @ Nashville PHP
  2. @epochblue

  3. why are we here?

  4. Let’s talk about... hot chicken

  5. a hot chicken ordering system Input order Order Heard Order

    Started Order Checked Order Finished Order Up Order delivered
  6. a hot chicken ordering system Input order updateOrder() updateOrder() updateOrder()

    updateOrder() updateOrder() Order delivered
  7. sounds simple enough, right? function updateOrder() { $status = $this->getStatus();

    switch($status) { case 'NEW': $this->setHeardTime(new \DateTime()); $this->setStatus('HEARD')->save(); break; // ... repeat for other states } }
  8. the marketing department appears

  9. email order updates function updateOrder() { $status = $this->getStatus(); switch($status)

    { case 'NEW': $this->setHeardTime(new \DateTime()); $this->setStatus('HEARD')->save(); break; // ... repeat for other states } $mailer = new Mailer( $this->getCustomer()->getEmail(), 'yourfriend@hattiebs.com', "Your order is now ${this->getStatus()}!" ); $mailer->send(); }
  10. more ideas appear

  11. and they want metrics function updateOrder() { $status = $this->getStatus();

    switch($status) { case 'NEW': $this->setHeardTime(new \DateTime()); $this->setStatus('HEARD')->save(); $db = $this->getReportingDB(); $db->timeToHeard( $this->getHeardTime() - $this->getCreatedTime() ); break; // ... repeat for other states } $mailer = new Mailer( $this->getCustomer()->getEmail(), 'yourfriend@hattiebs.com', "Your order is now ${this->getStatus()}!" ); $mailer->send(); }
  12. even more ideas appear Users should receive text messages as

    the state of their order changes Update a progress bar (Hi Josh!) on an UI somewhere on the line Every 10th order, the customer should be sent a satisfaction survey Every 50th order, send the customer a coupon for <something> And think of all the metrics: Count orders that fail quality check Count orders that use coupons and how much they paid Send email for orders taking longer than 15 minutes to fill etc etc etc
  13. AND before long YOU HAVE... function updateOrder() { $status =

    $this->getStatus(); switch($status) { case 'NEW': $this->setHeardTime(new \DateTime()); $this->setStatus('ACCEPTED')->save(); $report = $this->getReportingDB(); $db->timeToAccepted( $this->getAcceptedTime() - $this->getCreatedTime() ); break; // ... repeat for other states case 'HEARD': $this->setCheckedTime(new \DateTime()); $this->setStatus('STARTED')->save(); $report = $this->getReportingDB(); $db->timeToStarted( $this->getStartedTime() - $this->getHeardTime() ); break; case 'STARTED': $this->setCompletedTime(new \DateTime()); $this->setStatus('COMPLETED')->save(); $report = $this->getReportingDB(); $db->timeToChecked( $this->getCheckedTime() - $this->getStartedTime() ); break; case 'COMPLETED': $this->setUpTime(new \DateTime()); $this->setStatus('UP')->save(); $report = $this->getReportingDB(); $db->timeTothisUp( $this->getUpTime() - $this->getCompletedTime() ); break; case 'UP': $this->setDeliveredTime(new \DateTime()); $this->setStatus('DELIVERED')->save(); $report = $this->getReportingDB(); $db->timeToDelivered( $this->getDeliveredTime() - $this->getUpTime() ); break; } $mailer = new Mailer( $this->getCustomer()->getEmail(), 'yourfriend@hattiebs.com', "Your order is now ${this->getStatus()}!" ); $mailer->send(); } lolwut
  14. what do we have? We have an Order object... ...that

    needs to update as the order progresses... ...and when it updates, lots of things need to happen. we need EVENTS
  15. so what is an event? An event is an action

    detected by the system, whose occurrence is announced (the event is “fired”), and anything listening for the announcement performs some action (or actions) in response to the announcement (the event is “handled”). Huh?
  16. again, with pictures order.complete Event announcement doSomething() doSomething() doSomething() do

    something
  17. is there a pattern for this? yes! (two, in fact)

  18. observer pattern Two parts: Subject & Observer Simple interface: Observers

    “subscribe” to Subjects Subjects “notify” Observers of changes Observers can “unsubscribe” from Subjects if they no longer want updates subject observer subject observer attach() subject observer update() subject observer detach()
  19. mediator pattern Three parts: Subject, Observer, Mediator How it differs

    from Observer: Observers and Subjects communicate through the Mediator Has a similar interface: Observers “subscribe” to events thrown by the Mediator Subjects tell the Mediator to “publish” an event The Mediator “notifies” the subscribing Observers of the event subject observer mediator attach() update() publish()
  20. back to the hot chicken.

  21. what do we have now? We have an Order object...

    ...that needs to update as the order progresses... ...and when it updates, things need to happen... ...and now we need to change it to use events.
  22. splsubject and splobserver

  23. splsubject and splobserver interface SplSubject { public attach(\SplObserver $observer) public

    detach(\SplObserver $observer) public notify() } interface SplObserver { public update(\SplSubject $subject) }
  24. Step 0: what we have now

  25. where are we now? Order email update reporting DB

  26. Step 1: order becomes a subject

  27. order: now AS A subject class Order implements \SplSubject {

    private $observers = new \SplObjectStorage(); function attach(\SplObserver $observer) { $this->observers->attach($observer); } function detach(\SplObserver $observer) { $this->observers->detach($observer); } function notify() { foreach($this->observers as $o) { $o->update($this); } } // ...snip }
  28. Pst. it looks the same. Order email update reporting DB

  29. Step 2: email and report observers

  30. EMAIL: NOW AS AN OBSERVER // EmailNotifier.php class EmailNotifier implements

    \SplObserver { public function update(\SplSubject $subject) { $mailer = new Mailer( $subject->getCustomer()->getEmail(), 'yourfriend@hattiebs.com', "Your order is now ${subject->getStatus()}" ); $mailer->send(); } }
  31. reporting: NOW AS AN OBSERVER // OrderReporter.php class OrderReporter implements

    \SplObserver { public function update(\SplSubject $subject) { $status = $subject->getStatus(); $db = $this->getReportingDB(); switch($status) { case 'NEW': $db->timeToHeard( $subject->getHeardTime() - $subject->getCreatedTime() ); break; // ... repeat for other states }
  32. step 2 EmailNotifier OrderReporter Order email update reporting DB

  33. Step 3: wire ‘em up

  34. attach those observers // bootstrap.php $order = new Order(); $mail_observer

    = new EmailNotifier(...); $report_observer = new OrderReporter(...); $order->attach($mail_observer); $order->attach($report_observer);
  35. step 3 EmailNotifier OrderReporter Order email update reporting DB

  36. Step 4: simplify order updating

  37. updateorder(): before function updateOrder() { $status = $this->getStatus(); switch($status) {

    case 'NEW': $this->setHeardTime(new \DateTime()); $this->setStatus('HEARD')->save(); $report = $this->getReportingDB(); $db->timeToAccepted( $this->getHeardTime() - $this->getCreatedTime() ); break; // ... repeat for other states } $mailer = new Mailer( $this->getCustomer()->getEmail(), 'yourfriend@hattiebs.com', "Your order is now ${this->getStatus()}!" ); $mailer->send(); }
  38. updateorder(): after function updateOrder() { $status = $this->getStatus(); switch($status) {

    case 'NEW': $this->setHeardTime(new \DateTime()); $this->setStatus('HEARD')->save(); break; // ... repeat for other states } $this->notify($this); }
  39. side by side function updateOrder() { $status = $this->getStatus(); switch($status)

    { case 'NEW': $this->setHeardTime(new \DateTime()); $this->setStatus('ACCEPTED')->save(); $report = $this->getReportingDB(); $db->timeToAccepted( $this->getAcceptedTime() - $this->getCreatedTime() ); break; // ... repeat for other states case 'HEARD': $this->setCheckedTime(new \DateTime()); $this->setStatus('STARTED')->save(); $report = $this->getReportingDB(); $db->timeToStarted( $this->getStartedTime() - $this->getHeardTime() ); break; case 'STARTED': $this->setCompletedTime(new \DateTime()); $this->setStatus('COMPLETED')->save(); $report = $this->getReportingDB(); $db->timeToChecked( $this->getCheckedTime() - $this->getStartedTime() ); break; case 'COMPLETED': $this->setUpTime(new \DateTime()); $this->setStatus('UP')->save(); $report = $this->getReportingDB(); $db->timeTothisUp( $this->getUpTime() - $this->getCompletedTime() ); break; case 'UP': $this->setDeliveredTime(new \DateTime()); $this->setStatus('DELIVERED')->save(); $report = $this->getReportingDB(); $db->timeToDelivered( $this->getDeliveredTime() - $this->getUpTime() ); break; } $mailer = new Mailer( $this->getCustomer()->getEmail(), 'yourfriend@hattiebs.com', "Your order is now ${this->getStatus()}!" ); $mailer->send(); } function updateOrder() { $status = $this->getStatus(); switch($status) { case 'NEW': $this->setHeardTime(new \DateTime()); $this->setStatus('ACCEPTED')->save(); break; case 'HEARD': $this->setCheckedTime(new \DateTime()); $this->setStatus('STARTED')->save(); break; case 'STARTED': $this->setCompletedTime(new \DateTime()); $this->setStatus('COMPLETED')->save(); break; case 'COMPLETED': $this->setUpTime(new \DateTime()); $this->setStatus('UP')->save(); break; case 'UP': $this->setDeliveredTime(new \DateTime()); $this->setStatus('DELIVERED')->save(); break; } $this->notify($this); }
  40. and finally... Order EmailNotifier OrderReporter update()

  41. so what do we have now? Email notifications and the

    reporting DB updates are no longer coupled to the Order object Other objects now have a mechanism for telling an Order that they want to know when it updates Order now has a mechanism for telling other objects when it updates And neither need to know that the other exists Using events here... Made our order update code cleaner, more focused, better encapsulated Decoupled our objects from each other
  42. oh no! HERE COMES SCOPE CREEP!

  43. “customer loyalty program” // FreeChicken.php class FreeChicken implements \SplObserver {

    public function update(\SplSubject $subject) { if ($subject->getStatus !== 'COMPLETED') { return; } $customer = $subject->getCustomer(); $orders = count($customer->getOrders()); if ($orders > 0 && $orders % 50 === 0)) { $customer->sendCouponForFreeChicken(); } } }
  44. “customer loyalty program”, cont’d // bootstrap.php $order = new Order();

    $mail_observer = new EmailNotifier(...); $report_observer = new OrderReporter(...); $free_chicken = new FreeChicken(...); $order->attach($mail_observer); $order->attach($report_observer); $order->attach($free_chicken);
  45. let’s try again... Email notifications and the reporting DB updates

    are no longer coupled to the Order object Other objects now have a mechanism for telling an Order that they want to know when it updates Order now has a mechanism for telling other objects when it updates And neither needs to know that the other exists Using events here... Made our order update code cleaner, more focused, better encapsulated Decoupled our objects from each other Made changes/additions easier
  46. observer drawbacks?

  47. what about A mediator?

  48. no more subject // Mediator.php class Mediator { private $events

    = array(); public function attach($event, $observer) { if (isset($this->events[$event])) { $this->events[$event]->attach($observer); } else { $this->events[$event] = new \SplObjectStorage(); $this->events[$event]->attach($observer); } } // ... wait for it
  49. no more subject, CONT’d // ... wait for it public

    function detach($event, $observer) { if (isset($this->events[$event])) { $this->events[$event]->detach($observer); } } public function publish($event, $subject) { if (isset($this->events[$event])) { foreach($this->events[$event] as $ob) { $ob->update($subject); } } } }
  50. so what happened to the subject?

  51. see, there’s still a subject class Order { function updateOrder($order)

    { $status = $order->getStatus(); switch($status) { case 'NEW': $order->setAcceptedTime(new \DateTime()); $order->setStatus('ACCEPTED')->save(); break; // ...repeat for other states } $this->get('mediator')->publish('order.update', $this); } }
  52. what about the observers? actually, they stay the same.

  53. BUT HOOKing them UP IS DIFFERENT?

  54. mediator hook ups // bootstrap.php $dispatcher = new Mediator(); $mail_observer

    = new EmailNotifier( ... ); $report_observer = new OrderReporter( ... ); $free_chicken = new FreeChicken( ... ); $dispatcher->attach('order.update', $mail_observer); $dispatcher->attach('order.update', $report_observer); $dispatcher->attach('order.update', $free_chicken);
  55. OK. that was a lot. let’s recap.

  56. once more, with feeling Events help decouple and clean up

    code when you have many (or an unknown number) of objects that need to know when the state of an object changes. Think: UI code, state machines, saving objects, beginning/ending a process... Observer Pattern Simpler, but more limited Better for small numbers of objects, persistent object, or singletons SplSubject/SplObserver Mediator Pattern (Slightly) More Complicated, but more flexible Typically better for web applications Symfony’s EventDispatcher component
  57. MORE FUN WITH events

  58. more with events Task/Message queues Event Loops IRC Bots Web

    servers NodeJS (see also: ReactPHP) Event-Driven Architecture
  59. That’s it. Questions?

  60. THanks! twitter & github: @epochblue

  61. errata http://www.hattieb.com/ http://iostudio.com/ http://studionow.com/ http://en.wikipedia.org/wiki/Observer_pattern http://en.wikipedia.org/wiki/Mediator_pattern http://www.php.net/manual/en/class.splsubject.php http://www.php.net/manual/en/class.splobserver.php http://symfony.com/doc/current/components/event_dispatcher/introduction.html http://reactphp.org/

    https://github.com/epochblue/philip/