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.

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

    View Slide

  2. @epochblue

    View Slide

  3. why are we here?

    View Slide

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

    View Slide

  5. a hot chicken ordering system
    Input order
    Order Heard Order Started Order Checked Order Finished Order Up
    Order delivered

    View Slide

  6. a hot chicken ordering system
    Input order
    updateOrder() updateOrder() updateOrder() updateOrder() updateOrder()
    Order delivered

    View Slide

  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
    }
    }

    View Slide

  8. the marketing department appears

    View Slide

  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(),
    '[email protected]',
    "Your order is now ${this->getStatus()}!"
    );
    $mailer->send();
    }

    View Slide

  10. more ideas appear

    View Slide

  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(),
    '[email protected]',
    "Your order is now ${this->getStatus()}!"
    );
    $mailer->send();
    }

    View Slide

  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
    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

    View Slide

  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(),
    '[email protected]',
    "Your order is now ${this->getStatus()}!"
    );
    $mailer->send();
    }
    lolwut

    View Slide

  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

    View Slide

  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?

    View Slide

  16. again, with pictures
    order.complete
    Event
    announcement
    doSomething() doSomething() doSomething()
    do something

    View Slide

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

    View Slide

  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()

    View Slide

  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()

    View Slide

  20. back to the hot chicken.

    View Slide

  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.

    View Slide

  22. splsubject and splobserver

    View Slide

  23. splsubject and splobserver
    interface SplSubject {
    public attach(\SplObserver $observer)
    public detach(\SplObserver $observer)
    public notify()
    }
    interface SplObserver {
    public update(\SplSubject $subject)
    }

    View Slide

  24. Step 0: what we have now

    View Slide

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

    View Slide

  26. Step 1: order becomes a subject

    View Slide

  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
    }

    View Slide

  28. Pst. it looks the same.
    Order
    email update reporting DB

    View Slide

  29. Step 2: email and report observers

    View Slide

  30. EMAIL: NOW AS AN OBSERVER
    // EmailNotifier.php
    class EmailNotifier implements \SplObserver
    {
    public function update(\SplSubject $subject) {
    $mailer = new Mailer(
    $subject->getCustomer()->getEmail(),
    '[email protected]',
    "Your order is now ${subject->getStatus()}"
    );
    $mailer->send();
    }
    }

    View Slide

  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
    }

    View Slide

  32. step 2
    EmailNotifier OrderReporter
    Order
    email update reporting DB

    View Slide

  33. Step 3: wire ‘em up

    View Slide

  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);

    View Slide

  35. step 3
    EmailNotifier OrderReporter
    Order
    email update reporting DB

    View Slide

  36. Step 4: simplify order updating

    View Slide

  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(),
    '[email protected]',
    "Your order is now ${this->getStatus()}!"
    );
    $mailer->send();
    }

    View Slide

  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);
    }

    View Slide

  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(),
    '[email protected]',
    "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);
    }

    View Slide

  40. and finally...
    Order
    EmailNotifier OrderReporter
    update()

    View Slide

  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

    View Slide

  42. oh no!
    HERE COMES SCOPE CREEP!

    View Slide

  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();
    }
    }
    }

    View Slide

  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);

    View Slide

  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

    View Slide

  46. observer drawbacks?

    View Slide

  47. what about A mediator?

    View Slide

  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

    View Slide

  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);
    }
    }
    }
    }

    View Slide

  50. so what happened to the subject?

    View Slide

  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);
    }
    }

    View Slide

  52. what about the observers?
    actually, they stay the same.

    View Slide

  53. BUT HOOKing them UP IS DIFFERENT?

    View Slide

  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);

    View Slide

  55. OK. that was a lot. let’s recap.

    View Slide

  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

    View Slide

  57. MORE FUN WITH events

    View Slide

  58. more with events
    Task/Message queues
    Event Loops
    IRC Bots
    Web servers
    NodeJS (see also: ReactPHP)
    Event-Driven Architecture

    View Slide

  59. That’s it. Questions?

    View Slide

  60. THanks!
    twitter & github: @epochblue

    View Slide

  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/

    View Slide