Top Banner
Taming Command Bus Krzysztof Menżyk @kmenzyk
110

Taming Command Bus

Apr 13, 2017

Download

Technology

Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: Taming Command Bus

Taming

Command Bus

Krzysztof Menżyk @kmenzyk

Page 2: Taming Command Bus

About me

Technical Leader at Test infectedBelieves that software is a craftLoves domain modelling

Homebrewer & squash player

Page 3: Taming Command Bus

This talk is not

Page 4: Taming Command Bus

This talk is not

DDD*

Page 5: Taming Command Bus

This talk is not

CQRS*

Page 6: Taming Command Bus

Back to the

PAST

Page 7: Taming Command Bus

BEGANIt all

with ...

Page 8: Taming Command Bus

class ProductController extends Controller{ //...

public function updateAction($id) { if (false === $this->get('security.context')->isGranted('ROLE_ADMIN')) { throw new AccessDeniedException(); }

$em = $this->getDoctrine()->getManager(); $product = $em->getRepository('AcmeDemoBundle:Product')->find($id);

if (!$product) { throw $this->createNotFoundException('Unable to find Product entity.'); }

$request = $this->getRequest(); $editForm = $this->createForm(new ProductType(), $product); $editForm->bind($request);

if ($editForm->isValid()) { $product->setLastUpdated(new \DateTime); $em->flush();

$message = \Swift_Message::newInstance() ->setSubject('Product updated') ->setFrom($this->container->getParameter('acme.product_email.from')) ->setTo($this->container->getParameter('acme.product_email.to')) ->setBody($this->renderView( 'AcmeDemoBundle:Product:email.txt.twig', array('product' => $product)) ) ; $this->get('mailer')->send($message);

return $this->redirect( $this->generateUrl('product', array('id' => $id)) ); }

return new Response( $this->renderView( 'AcmeDemoBundle:Product:edit.html.twig', array( 'product' => $product, 'edit_form' => $editForm->createView(), ) ) ); }}

Page 9: Taming Command Bus

Controller Driven Design

„approach”

Page 10: Taming Command Bus

class ProductController extends Controller{ //...

public function updateAction($id) { if (false === $this->get('security.context')->isGranted('ROLE_ADMIN')) { throw new AccessDeniedException(); }

$em = $this->getDoctrine()->getManager(); $product = $em->getRepository('AcmeDemoBundle:Product')->find($id);

if (!$product) { throw $this->createNotFoundException('Unable to find Product entity.'); }

$request = $this->getRequest(); $editForm = $this->createForm(new ProductType(), $product); $editForm->bind($request);

if ($editForm->isValid()) { $product->setLastUpdated(new \DateTime); $em->flush();

$message = \Swift_Message::newInstance() ->setSubject('Product updated') ->setFrom($this->container->getParameter('acme.product_email.from')) ->setTo($this->container->getParameter('acme.product_email.to')) ->setBody($this->renderView( 'AcmeDemoBundle:Product:email.txt.twig', array('product' => $product)) ) ; $this->get('mailer')->send($message);

return $this->redirect( $this->generateUrl('product', array('id' => $id)) ); }

return new Response( $this->renderView( 'AcmeDemoBundle:Product:edit.html.twig', array( 'product' => $product, 'edit_form' => $editForm->createView(), ) ) ); }}

Page 11: Taming Command Bus

class ProductController extends Controller{ //...

public function updateAction($id) { if (false === $this->get('security.context')->isGranted('ROLE_ADMIN')) { throw new AccessDeniedException(); }

$em = $this->getDoctrine()->getManager(); $product = $em->getRepository('AcmeDemoBundle:Product')->find($id);

if (!$product) { throw $this->createNotFoundException('Unable to find Product entity.'); }

$request = $this->getRequest(); $editForm = $this->createForm(new ProductType(), $product); $editForm->bind($request);

if ($editForm->isValid()) { $product->setLastUpdated(new \DateTime); $em->flush();

$message = \Swift_Message::newInstance() ->setSubject('Product updated') ->setFrom($this->container->getParameter('acme.product_email.from')) ->setTo($this->container->getParameter('acme.product_email.to')) ->setBody($this->renderView( 'AcmeDemoBundle:Product:email.txt.twig', array('product' => $product)) ) ; $this->get('mailer')->send($message);

return $this->redirect( $this->generateUrl('product', array('id' => $id)) ); }

return new Response( $this->renderView( 'AcmeDemoBundle:Product:edit.html.twig', array( 'product' => $product, 'edit_form' => $editForm->createView(), ) ) ); }}

Page 12: Taming Command Bus

LAYERS

Page 13: Taming Command Bus

Presentation

Page 14: Taming Command Bus

Presentation

Domain

Page 15: Taming Command Bus

Presentation

Service

Domain

Page 16: Taming Command Bus

Presentation

Service

Domain

Infrastructure

Page 17: Taming Command Bus

Presentation

Service

Domain

Infrastructure

Page 18: Taming Command Bus

Defines an application's boundary with a layer of services.

Service Layer

Page 19: Taming Command Bus

Establishes set of available operations.

Service Layer

Page 20: Taming Command Bus

Coordinates the application's response in each operation.

Service Layer

Page 21: Taming Command Bus

USE Cases

Page 22: Taming Command Bus

USER STORIES

Page 23: Taming Command Bus

Application Layer

Page 24: Taming Command Bus

class Library{ public function borrowBook($bookId, $readerId) { // ... }}

Page 25: Taming Command Bus

class LibraryController{ public function borrowAction(ServerRequestInterface $request) { // Forms, etc.

$this->library->borrowBook($bookId, $readerId); // Response }}

Page 26: Taming Command Bus

class Library{ public function borrowBook($bookId, $readerId) { if (null === $bookId || null === $readerId) { throw new \InvalidArgumentException(); }

$this->logging->debug('A reader is borrowing a book');

$this->connection->beginTransaction(); try { $book = $this->books->get($bookId); $book->borrowBy($readerId); $this->books->add($book); $this->connection->commit(); } catch (\Exception $e) { $this->connection->rollBack(); $this->logging->debug('Ooops! Bad luck.'); throw $e; }

$this->logging->debug('Success!'); }}

Page 27: Taming Command Bus

class Library{ public function borrowBook($bookId, $readerId) { if (null === $bookId || null === $readerId) { throw new \InvalidArgumentException(); }

$this->logging->debug('A reader is borrowing a book');

$this->connection->beginTransaction(); try { $book = $this->books->get($bookId); $book->borrowBy($readerId); $this->books->add($book); $this->connection->commit(); } catch (\Exception $e) { $this->connection->rollBack(); $this->logging->debug('Ooops! Bad luck.'); throw $e; }

$this->logging->debug('Success!'); }}

Page 28: Taming Command Bus

class Library{ public function borrowBook($bookId, $readerId) { if (null === $bookId || null === $readerId) { throw new \InvalidArgumentException(); }

$this->logging->debug('A reader is borrowing a book');

$this->connection->beginTransaction(); try { $book = $this->books->get($bookId); $book->borrowBy($readerId); $this->books->add($book); $this->connection->commit(); } catch (\Exception $e) { $this->connection->rollBack(); $this->logging->debug('Ooops! Bad luck.'); throw $e; }

$this->logging->debug('Success!'); }}

Page 29: Taming Command Bus

class Library{ public function borrowBook($bookId, $readerId) { if (null === $bookId || null === $readerId) { throw new \InvalidArgumentException(); }

$this->logging->debug('A reader is borrowing a book');

$this->connection->beginTransaction(); try { $book = $this->books->get($bookId); $book->borrowBy($readerId); $this->books->add($book); $this->connection->commit(); } catch (\Exception $e) { $this->connection->rollBack(); $this->logging->debug('Ooops! Bad luck.'); throw $e; }

$this->logging->debug('Success!'); }}

Page 30: Taming Command Bus

class Library{ public function borrowBook($bookId, $readerId) { if (null === $bookId || null === $readerId) { throw new \InvalidArgumentException(); }

$this->logging->debug('A reader is borrowing a book');

$this->connection->beginTransaction(); try { $book = $this->books->get($bookId); $book->borrowBy($readerId); $this->books->add($book); $this->connection->commit(); } catch (\Exception $e) { $this->connection->rollBack(); $this->logging->debug('Ooops! Bad luck.'); throw $e; }

$this->logging->debug('Success!'); }}

Page 31: Taming Command Bus

borrowBook($bookId, $readerId)

Page 32: Taming Command Bus

new BorrowBook($bookId, $readerId)

Page 33: Taming Command Bus

COMMAND

Page 34: Taming Command Bus

not a class/objectMESSAGE

Page 35: Taming Command Bus

Messaging Flavors

by @mathiasverraes

Page 36: Taming Command Bus

Messaging Flavors

by @mathiasverraes

Imperative

Page 37: Taming Command Bus

Messaging Flavors

by @mathiasverraes

Imperative

Command

Page 38: Taming Command Bus

Messaging Flavors

by @mathiasverraes

Imperative Interrogatory

Command

Page 39: Taming Command Bus

Messaging Flavors

by @mathiasverraes

Imperative Interrogatory

Command QUERY

Page 40: Taming Command Bus

Messaging Flavors

by @mathiasverraes

Imperative Interrogatory Informational

Command QUERY

Page 41: Taming Command Bus

Messaging Flavors

by @mathiasverraes

Imperative Interrogatory Informational

Command QUERY EVENT

Page 42: Taming Command Bus

Messaging Flavors

by @mathiasverraes

Imperative Interrogatory Informational

Command QUERY EVENT

Page 43: Taming Command Bus

Captures

THE INTENTof the user

Page 44: Taming Command Bus

Supports

UbiquitousLANGUAGE

Page 45: Taming Command Bus

Borrow BookGive Book BackRegister User

Start SubscriptionHire Employee

Page 46: Taming Command Bus

Contains

THE INPUTto carry out the task

Page 47: Taming Command Bus

final class BorrowBook{ public $bookId; public $readerId;

public function __construct($bookId, $readerId) { $this->bookId = $bookId; $this->readerId = $readerId; }}

Page 48: Taming Command Bus

Should be

IMMUTABLE

Page 49: Taming Command Bus

final class BorrowBook{ private $bookId; private $readerId;

public function __construct($bookId, $readerId) { $this->bookId = $bookId; $this->readerId = $readerId; }

public function getBookId() { return $this->bookId; }

public function getReaderId() { return $this->readerId; }}

Page 50: Taming Command Bus

Must be

VALID

Page 51: Taming Command Bus

final class BorrowBook{ private $bookId; private $readerId;

public function __construct($bookId, $readerId) { Assertion::notNull($bookId); Assertion::notNull($readerId); $this->bookId = $bookId; $this->readerId = $readerId; }

public function getBookId() { return $this->bookId; }

public function getReaderId() { return $this->readerId; }}

Page 52: Taming Command Bus

INTERPRETHow to

a command?

Page 53: Taming Command Bus

COMMANDHandler

Page 54: Taming Command Bus

per commandONE HANDLER

Page 55: Taming Command Bus

Returns

NO VALUE

Page 56: Taming Command Bus

final class BorrowBookHandler{ public function handle(BorrowBook $command) { $this->logging->debug('A reader is borrowing a book');

$this->connection->beginTransaction();

try { $book = $this->books->get($command->getBookId()); $book->borrowBy($command->getReaderId());

$this->books->add($book);

$this->connection->commit(); } catch (\Exception $e) { $this->connection->rollBack();

$this->logging->debug('Ooops! Bad luck.');

throw $e; }

$this->logging->debug('Success!'); }}

Page 57: Taming Command Bus

final class BorrowBookHandler{ public function handle(BorrowBook $command) { $this->logging->debug('A reader is borrowing a book');

$this->connection->beginTransaction();

try { $book = $this->books->get($command->getBookId()); $book->borrowBy($command->getReaderId());

$this->books->add($book);

$this->connection->commit(); } catch (\Exception $e) { $this->connection->rollBack();

$this->logging->debug('Ooops! Bad luck.');

throw $e; }

$this->logging->debug('Success!'); }}

Page 58: Taming Command Bus

One more thing ...

Page 59: Taming Command Bus

The BUS

Page 60: Taming Command Bus

COMMANDBUS

Page 61: Taming Command Bus

Hands over

THE COMMANDto

THE HANDLEr

Page 62: Taming Command Bus

Handler C

CommandBus

Handler BHandler A

Page 63: Taming Command Bus

Handler C

Command C

CommandBus

Handler BHandler A

Page 64: Taming Command Bus

Handler C

CommandBus

Handler BHandler A

Command C

Page 65: Taming Command Bus

Handler C

CommandBus

Handler BHandler A Command C

Page 66: Taming Command Bus

Handler C

CommandBus

Handler BHandler A

Page 67: Taming Command Bus

Single

ENTRY POINTto the application

Page 68: Taming Command Bus

interface CommandBus{ public function handle($command);}

Page 69: Taming Command Bus

class LibraryController{ public function borrowAction(ServerRequestInterface $request) { // Forms, etc.

$this->commandBus->handle(new BorrowBook($bookId, $readerId));

// Response }}

Page 70: Taming Command Bus

class SomeOtherController{ public function someOtherAction(ServerRequestInterface $request) { // Forms, etc.

$command = $form->getData(); $this->commandBus->handle($command);

// Response }}

Page 71: Taming Command Bus

class SomeOtherController{ public function someOtherAction(ServerRequestInterface $request) { // Forms, etc.

$command = $form->getData(); $this->commandBus->handle($command);

// Response }}

Page 72: Taming Command Bus

class BorrowBookCli{ protected function execute(InputInterface $input, OutputInterface $output) { // get $command from the input somehow $this->commandBus->handle($command);

// ... }}

Page 73: Taming Command Bus

IMPLEMENTLet's

the stupid thing

Page 74: Taming Command Bus

class SimpleCommandBus implements CommandBus{ private $handlers;

public function __construct(array $handlers) { $this->handlers = $handlers; }

public function handle($command) { $commandName = get_class($command);

if (!isset($this->handlers[$commandName])) { throw new \InvalidArgumentException('No handler'); } $this->handlers[$commandName]->handle($command); }}

Page 75: Taming Command Bus

new SimpleCommandBus([ BorrowBook::class => $borrowBookHandler, GiveBookBack::class => $giveBookBackHandler,]);

Page 76: Taming Command Bus

That's it?

Page 77: Taming Command Bus

Do not

TRY ITat home

Page 78: Taming Command Bus

class SimpleCommandBus implements CommandBus{ private $handlers;

public function __construct(array $handlers) { $this->handlers = $handlers; }

public function handle($command) { $commandName = get_class($command);

if (!isset($this->handlers[$commandName])) { throw new \InvalidArgumentException('No handler'); } $this->handlers[$commandName]->handle($command); }}

Page 79: Taming Command Bus

Cross cutting concerns

Page 80: Taming Command Bus

interface CommandBus{ public function handle($command);}

Page 81: Taming Command Bus

DECORATOR

Page 82: Taming Command Bus

class CommandBusWithAddedBehavior implements CommandBus{ public function __construct(CommandBus $originalCommandBus) { $this->originalCommandBus = $originalCommandBus; }

public function handle($command) { // do anything you want

$this->originalCommandBus->handle($command);

// do even more }}

Page 83: Taming Command Bus

new CommandBusWithAddedBehavior( new SimpleCommandBus([ BorrowBook::class => $borrowBookHandler, GiveBookBack::class => $giveBookBackHandler, ]));

Page 84: Taming Command Bus

Command Bus

Decorator

Decorator

Command

Page 85: Taming Command Bus

final class BorrowBookHandler{ public function handle(BorrowBook $command) { $this->logging->debug('A reader is borrowing a book');

$this->connection->beginTransaction();

try { $book = $this->books->get($command->getBookId()); $book->borrowBy($command->getReaderId());

$this->books->add($book);

$this->connection->commit(); } catch (\Exception $e) { $this->connection->rollBack();

$this->logging->debug('Ooops! Bad luck.');

throw $e; }

$this->logging->debug('Success!'); }}

Page 86: Taming Command Bus

class TransactionalCommandBus implements CommandBus{ public function __construct($innerCommandBus, $connection) { $this->innerCommandBus = $innerCommandBus; $this->connection = $connection; }

public function handle($command) { $this->connection->beginTransaction(); try { $this->innerCommandBus->handle($command); $this->connection->commit(); } catch (\Exception $exception) { $this->connection->rollBack();

throw $e; } }}

Page 87: Taming Command Bus

final class BorrowBookHandler{ public function handle(BorrowBook $command) { $this->logging->debug('A reader is borrowing a book');

$this->connection->beginTransaction();

try { $book = $this->books->get($command->getBookId()); $book->borrowBy($command->getReaderId());

$this->books->add($book);

$this->connection->commit(); } catch (\Exception $e) { $this->connection->rollBack();

$this->logging->debug('Ooops! Bad luck.');

throw $e; }

$this->logging->debug('Success!'); }}

Page 88: Taming Command Bus

class LoggingCommandBus implements CommandBus{ public function __construct($innerCommandBus, $logger) { $this->innerCommandBus = $innerCommandBus; $this->logger = $logger; }

public function handle($command) { $commandClass = get_class($command);

$this->logger->debug('Started handling ' . $commandClass);

try { $this->innerCommandBus->handle($command); } catch (\Exception $exception) { $this->logger->debug('Error while handling ' . $commandClass);

throw $e; }

$this->logger->debug('Finished handling ' . $commandClass); }}

Page 89: Taming Command Bus

final class BorrowBookHandler{ public function handle(BorrowBook $command) { $this->logging->debug('A reader is borrowing a book');

$this->connection->beginTransaction();

try { $book = $this->books->get($command->getBookId()); $book->borrowBy($command->getReaderId());

$this->books->add($book);

$this->connection->commit(); } catch (\Exception $e) { $this->connection->rollBack();

$this->logging->debug('Ooops! Bad luck.');

throw $e; }

$this->logging->debug('Success!'); }}

Page 90: Taming Command Bus

final class BorrowBookHandler{ public function handle(BorrowBook $command) { $book = $this->books->get($command->getBookId()); $book->borrowBy($command->getReaderId());

$this->books->add($book);'Success!'); }}

Page 91: Taming Command Bus

Easy to

TEST

Page 92: Taming Command Bus

But...

Page 93: Taming Command Bus

new LoggingCommandBus( new TransactionalCommandBus( new SimpleCommandBus([ BorrowBook::class => $borrowBookHandler, GiveBookBack::class => $giveBookBackHandler, ]), $connection ), $logger);

Page 94: Taming Command Bus

Can we do better?

Page 95: Taming Command Bus

Yes

We can!

Page 96: Taming Command Bus

CHAIN OF RESPONSIBILITY

Page 97: Taming Command Bus

interface CommandBusMiddleware{ public function handle($command, callable $next);}

Page 98: Taming Command Bus

Command Handler

Middleware

Command

Middleware

Middleware

Page 99: Taming Command Bus

class AddedBehaviorMiddleware implements CommandBusMiddleware{ public function handle($command, callable $next) { // do anything you want

$next($command);

// do even more }}

Page 100: Taming Command Bus

new CommandBusSupportingMiddleware([ new AddedBehaviorMiddleware(), new CommandHandlerMiddleware([ BorrowBook::class => $borrowBookHandler, GiveBookBack::class => $giveBookBackHandler, ])]);

Page 101: Taming Command Bus

new CommandBusSupportingMiddleware([ new LoggingMiddleware($logger), new TransactionalMiddleware($connection), new CommandHandlerMiddleware([ BorrowBook::class => $borrowBookHandler, GiveBookBack::class => $giveBookBackHandler, ])]);

Page 102: Taming Command Bus

TransactionsLoggingSecurity

Performance MetricsAudit log

You name it

Page 103: Taming Command Bus

Command Bus libraries

Page 104: Taming Command Bus

by @matthiasnobackSimple Bus

Page 105: Taming Command Bus

by @rosstuckTACTICIAN

Page 106: Taming Command Bus

by @qandidate-labsBROADWAY

Page 107: Taming Command Bus

Use them and contribute

Page 108: Taming Command Bus

Like we

DID

Page 109: Taming Command Bus

Krzysztof Menżyk @kmenzyk

Thanks!

Taming Command Bus

Page 110: Taming Command Bus

PHOTO CREDITShttps://flic.kr/p/F2qhc7https://flic.kr/p/zMzVzWhttps://flic.kr/p/F4j2fHhttps://flic.kr/p/5a5d3bhttps://flic.kr/p/vXAK3D