Slide 1

Slide 1 text

Event Driven Applications Wrangle Cross-Cutting Concerns

Slide 2

Slide 2 text

Chris Saylor Lead Engineer @zumbatech www.chris-saylor.com @cjsaylor

Slide 3

Slide 3 text

A little about Zumba ● 180 Countries ● 15 million class participants ● 60 million service requests per month

Slide 4

Slide 4 text

Indicates my opinion on a subject. Feel free to discuss over beers.

Slide 5

Slide 5 text

What are cross-cutting concerns? Cross-cutting concerns are parts of a program that rely on or must affect many other parts of the system.

Slide 6

Slide 6 text

Cross-cutting examples ● Logging ● Caching ● Product feature interaction ● Monitoring ● Record audit trail ● State Management

Slide 7

Slide 7 text

class Cart { public function addItem(Item $item) { $this->cart->add($item); $this->cart->save(); } }

Slide 8

Slide 8 text

class Cart { public function addItem(Item $item) { $this->cart->add($item); if ($this->promo->isApplicable($this->cart, $item)) { $this->promo->applyItemPromo($item); } $this->cart->save(); } }

Slide 9

Slide 9 text

class Cart { public function addItem(Item $item) { if (!$this->inventory->isAvailable($item)) { throw new ItemUnavailable(); } $this->cart->add($item); if ($this->promo->isApplicable($this->cart, $item)) { $this->promo->applyItemPromo($item); } $this->cart->save(); } }

Slide 10

Slide 10 text

class Cart { public function addItem(Item $item) { if (!$this->inventory->isAvailable($item)) { throw new ItemUnavailable(); } $this->cart->add($item); if ($this->promo->isApplicable($this->cart, $item)) { $this->promo->applyItemPromo($item); } $this->inventory->reduceEphemeral($item); $this->cart->save(); } }

Slide 11

Slide 11 text

class Cart { public function addItem(Item $item) { if (!$this->inventory->isAvailable($item)) { throw new ItemUnavailable(); } if (!$this->catalog->isUserAllowed($this->cart, $item)) { throw new ItemNotAllowed(); } $this->cart->add($item); if ($this->promo->isApplicable($this->cart, $item)) { $this->promo->applyItemPromo($item); } $this->inventory->reduceEphemeral($item); $this->cart->save(); } }

Slide 12

Slide 12 text

Our cart is overloaded!

Slide 13

Slide 13 text

Cyclomatic Complexity A measure of the complexity by the number of linearly independent paths through the program. M = E - N + 2P E = the number of edges of the graph N = the number of nodes of the graph P = The number of connected components

Slide 14

Slide 14 text

class Cart { public function addItem(Item $item) { $this->cart->add($item); $this->cart->save(); } }

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

3 - 4 + 2 * 1 = 1

Slide 17

Slide 17 text

class Cart { public function addItem(Item $item) { if (!$this->inventory->isAvailable($item)) { throw new ItemUnavailable(); } if (!$this->catalog->isUserAllowed($this->cart, $item)) { throw new ItemNotAllowed(); } $this->cart->add($item); if ($this->promo->isApplicable($this->cart, $item)) { $this->promo->applyItemPromo($item); } $this->inventory->reduceEphemeral($item); $this->cart->save(); } }

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

13 - 11 + (2 * 1) = 4 Four times as much complexity!

Slide 20

Slide 20 text

Risks of cross-cutting concerns ● Coupling systems too tightly. ● Lots of code duplication. ● Brittle Test Cases.

Slide 21

Slide 21 text

Cart Promo Inventory Catalog

Slide 22

Slide 22 text

Cart Promo Inventory Catalog Changing Promo can affect Cart.

Slide 23

Slide 23 text

class Cart { public function addItem(Item $item) { if (!$this->inventory->isAvailable($item)) { throw new ItemUnavailable(); } if (!$this->catalog->isUserAllowed($this->cart, $item)) { throw new ItemNotAllowed(); } $this->cart->add($item); if ($this->promo->isPromo($item)) { $this->promo->applyItemPromo($item); } $this->cart->save(); $this->inventory->reduceEphemeral($item); } }

Slide 24

Slide 24 text

class Cart { public function updateItem(Item $item, $line) { if (!$this->inventory->isAvailable($item)) { throw new ItemUnavailable(); } if (!$this->catalog->isUserAllowed($this->cart, $item)) { throw new ItemNotAllowed(); } $this->cart->update($item, $line); if ($this->promo->isPromo($item)) { $this->promo->applyItemPromo($item); } $this->cart->save(); $this->inventory->reduceEphemeral($item); } }

Slide 25

Slide 25 text

class Cart { public function updateItem(Item $item, $line) { if (!$this->inventory->isAvailable($item)) { throw new ItemUnavailable(); } if (!$this->catalog->isUserAllowed($this->cart, $item)) { throw new ItemNotAllowed(); } $this->cart->update($item, $line); if ($this->promo->isPromo($item)) { $this->promo->applyItemPromo($item); } $this->cart->save(); $this->inventory->reduceEphemeral($item); } } 11 of 13 lines (85%) are duplicated from addItem!

Slide 26

Slide 26 text

Unit tests become unmaintainable and complex. class CartTest extends PHPUnit_Framework_TestCase { public function testCanAddItem() { // Mock promo // Mock inventory // Mock catalog } public function testUnableToAddItemFromCatalogUserLimit() { // More mocks } public function testAddItemWithPromotionRelated() { // Ugh... } public function testUnableToAddItemFromUnavailability() { // ... } }

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

Event Patterns Observer Pattern Mediator Pattern

Slide 29

Slide 29 text

Subject Observer Observer Observer Observer Observer Pattern

Slide 30

Slide 30 text

No content

Slide 31

Slide 31 text

class Subject implements \SplSubject { private $observers; public function __construct() { $this->observers = new \SplObjectStorage(); } public function attach(\SplObserver $observer) { $this->observers->attach($observer); } public function detach(\SplObserver $observer) { $this->observers->detach($observer); } public function notify() { foreach ($this->observers as $observer) { $observer->update($this)); } } }

Slide 32

Slide 32 text

No content

Slide 33

Slide 33 text

class Observer implements \SplObserver { public function update(\SplSubject $subject) { echo 'Observer notified!'; } }

Slide 34

Slide 34 text

$observer = new Observer(); $subject = new Subject(); $subject->attach($observer); $subject->notify(); // Observer notified! The subject has the responsibility of the Mediator.

Slide 35

Slide 35 text

Mediator Pattern Subject Mediator Observer Observer Subscribe Subscribe

Slide 36

Slide 36 text

Separation of Concerns Cart Promo Handler Inventory Handler Catalog Handler

Slide 37

Slide 37 text

Separation of Concerns Cart Promo Handler Inventory Handler Catalog Handler Event Bus

Slide 38

Slide 38 text

Synchronous Separation of Concerns ● Provides for modularity ● Allow for unit testing handlers separately ● Allows for priority and interrupts

Slide 39

Slide 39 text

class Cart { public function addItem(Item $item) { if (!$this->inventory->isAvailable($item)) { throw new ItemUnavailable(); } if (!$this->catalog->isUserAllowed($this->cart, $item)) { throw new ItemNotAllowed(); } $this->cart->add($item); if ($this->promo->isApplicable($this->cart, $item)) { $this->promo->applyItemPromo($item); } $this->inventory->reduceEphemeral($item); $this->cart->save(); } }

Slide 40

Slide 40 text

● Always be descriptive with event names. ● Always name them in past tense. ● Don’t emit the same event from more than one location. Event Conventions

Slide 41

Slide 41 text

class Cart { public function addItem(Item $item) { $this->dispatcher->dispatch( 'cart.addItemStarted', new GenericEvent($this->cart, compact('item')); ); $this->cart->add($item); if ($this->promo->isApplicable($this->cart, $item)) { $this->promo->applyItemPromo($item); } $this->inventory->reduceEphemeral($item); $this->cart->save(); } }

Slide 42

Slide 42 text

class Cart { public function addItem(Item $item) { $this->dispatcher->dispatch( 'cart.addItemStarted', new GenericEvent($this->cart, compact('item')); ); $this->cart->add($item); $this->dispatcher->dispatch( 'cart.itemAdded', new GenericEvent($this->cart, compact('item')); ); $this->cart->save(); } }

Slide 43

Slide 43 text

class Cart { private function dispatch($name, array $payload = []) { return $this->dispatcher->dispatch( 'cart.' . $name, new GenericEvent($this->cart, $payload); ); } public function addItem(Item $item) { $this->dispatch('addItemStarted', compact('item')); $this->cart->add($item); $this->dispatch('itemAdded', compact('item')); $this->cart->save(); } } Almost back to the original simple addItem.

Slide 44

Slide 44 text

5 - 6 + (2 * 1) = 1

Slide 45

Slide 45 text

Cart Promo Inventory Catalog

Slide 46

Slide 46 text

Cart Promo Inventory Catalog Inversion of responsibility!

Slide 47

Slide 47 text

Cart Promo Inventory Catalog Riskiest piece now changes less frequently.

Slide 48

Slide 48 text

class CartTest extends PHPUnit_Framework_TestCase { public function testCanAddItem() { // Mock promo // Mock inventory // Mock catalog } public function testUnableToAddItemFromCatalogUserLimit() { // More mocks } public function testAddItemWithPromotionRelated() { // Ugh... } public function testUnableToAddItemFromUnavailability() { // ... } }

Slide 49

Slide 49 text

class CartTest extends PHPUnit_Framework_TestCase { public function testAddItem() { // Mock dispatcher and spy on dispatch } }

Slide 50

Slide 50 text

class InventoryHandler implements EventSubscriberInterface { }

Slide 51

Slide 51 text

class InventoryHandler implements EventSubscriberInterface { // construct the inventory manager... public static function getSubscribedEvents() { return [ ‘cart.addItemStarted’ => ‘checkAvailability’ ]; } }

Slide 52

Slide 52 text

class InventoryHandler implements EventSubscriberInterface { // construct the inventory manager... public static function getSubscribedEvents() { return [ ‘cart.addItemStarted’ => ‘checkAvailability’ ]; } public function checkAvailability(Event $e) { $item = $e->getArgument(‘item’); if (!$this->inventory->isAvailable($item)) { throw new ItemUnavailable(); } } }

Slide 53

Slide 53 text

Asynchronous ● Synchronous in order of execution. ● Asynchronous in order of completion. $('p') .on('click', function() { setTimeout(function() { console.log('first called, second to finish!'); }, 1000); }) .on('click', function() { console.log('second called, first to finish.'); });

Slide 54

Slide 54 text

Synchronous ● Predictable order of execution and completion. $event ->on('event1', function() { echo 'Always first.'; sleep(1); }) ->on('event1', function() { echo 'Always second.'; });

Slide 55

Slide 55 text

Events in the Wild

Slide 56

Slide 56 text

● Symfony 2 ● Laravel ● CakePHP ● Lithium ● Yii ● Zend Framework ● Doctrine ● Guzzle ● Magento Events in the Wild

Slide 57

Slide 57 text

Advantages & Drawbacks ● Modular design ● Separation of Concerns ● Side effects are not obvious from looking at code. ● Source of event is not always obvious when debugging. Decoupling Code

Slide 58

Slide 58 text

Advantages & Drawbacks ● Allows third parties to hook in and modify behavior without modifying the core. ● Easy to add/remove hooks (even at runtime). ● No dependencies should exist between plugin hooks. Plugin Architecture

Slide 59

Slide 59 text

Advantages & Drawbacks ● Test handlers separate from the core functionality. ● Allows for execution of events from manual triggers. ● Testing interaction of handlers is cumbersome (functional testing). ● Unbinding specific event callbacks can be difficult. Testing

Slide 60

Slide 60 text

Conclusions ● We can take a relatively complex system and simplify key systems. ● We can remove concerns from key systems. ● We can modularize concerns into a containable / testable unit. ● We can allow for plugin architectures.

Slide 61

Slide 61 text

Further Reading ● Use application events to hook in plugins ● Decoupling applications with domain events ● Domain Events (Martin Fowler)

Slide 62

Slide 62 text

Questions?

Slide 63

Slide 63 text

Thanks!

Slide 64

Slide 64 text

Please leave feedback! https://joind.in/14244