Successfully reported this slideshow.
Your SlideShare is downloading. ×

Decoupling the Ulabox.com monolith. From CRUD to DDD

Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Loading in …3
×

Check these out next

1 of 83 Ad

More Related Content

Slideshows for you (20)

Advertisement

Similar to Decoupling the Ulabox.com monolith. From CRUD to DDD (20)

Recently uploaded (20)

Advertisement

Decoupling the Ulabox.com monolith. From CRUD to DDD

  1. 1. Decoupling Ulabox.com monolith From CRUD to DDD
  2. 2. Aleix Vergés Backend developer Scrum Master @avergess aleix.verges@ulabox.com
  3. 3. 1. What’s Ulabox?
  4. 4. 2. Decoupling. Why?
  5. 5. /** * @param Cart $cart * @return Order $order */ public function createOrder(Cart $cart) { // Year 2011 $order = $this->moveCartToOrder($cart); $this->sendConfirmationEmail($order); $this->reindexSolr($order); $this->sendToWarehouse($order); // Year 2012 $this->sendToFinantialErp($order); $this->sendDonationEmail($order); // Year 2014 $this->sendToDeliveryRoutingSoftware($order); // Year 2015 $this->sendToJustInTimeSuppliers($order); // Year 2016 $this->sendToWarehouseSpoke($order); // WTF! $this->sendToShipLoadSoftware($order); // WTF!!!! } Decoupling Ulabox.com monolith. From CRUD to DDD Our problem
  6. 6. WTF!
  7. 7. Past ● CRUD doesn’t make sense anymore ● It had sense at the beginning ● Product, Logistic, Delivery, Cart, Customers, ... ● It’s not sustainable. Decoupling Ulabox.com monolith. From CRUD to DDD
  8. 8. Our solution /** * @param Cart $cart */ public function createOrder(Cart $cart) { $createOrder = new CreateOrderCommand(Cart $cart); $this->commandBus->dispatch($createOrder) } Event bus CreateOrder command OrderWasCreated event subscribe subscribe subscribe subscribe subscribe subscribe subscribe subscribe subscribe Decoupling Ulabox.com monolith. From CRUD to DDD
  9. 9. 3. The tools
  10. 10. ● Domain ● Aggregate / Aggregate Root ● Repository ● Domain Events ● Service ● Command Bus ● Event Bus * Domain Drive Design: https://en.wikipedia.org/wiki/Domain-driven_design Decoupling Ulabox.com monolith. From CRUD to DDD
  11. 11. 4. A responsability question
  12. 12. Refactoring and manage technical debt is not a choice, but a responsability Decoupling Ulabox.com monolith. From CRUD to DDD
  13. 13. 5. Controllers
  14. 14. REFUND!
  15. 15. 5.1. OrderController 5. Controllers
  16. 16. class OrderController extends BaseController { public function refundAction(Request $request, $id) { $em = $this->container->get('doctrine.orm.entity_manager'); $orderPayment = $em->getRepository('UlaboxCoreBundle:OrderPayment')->find($id); $amount = $request->request->get('refund'); $data = $this->container->get('sermepa')->processRefund($orderPayment, $amount); $orderRefund = new OrderPayment(); $orderRefund->setAmount($amount); ... $em->persist($orderRefund); $em->flush(); return $this->redirectToRoute('order_show', ['id' => $orderPayment->getOrder()->getId()]); } public function someOtherAction(Request $request, $id) ... }
  17. 17. Decoupling Ulabox.com monolith. From CRUD to DDD Problems ● Hidden dependencies ● Inheritance. ● Biz logic in the controller. ● Non aggregate root. ● Difficult to test.
  18. 18. * Dependency Injection: https://es.wikipedia.org/wiki/Inyecci%C3%B3n_de_dependencias Decoupling Ulabox.com monolith. From CRUD to DDD Solutions ● Dependency Injection ● Break inheritance from base controller. ● Application services. ● Testing
  19. 19. 5.2. Controller as a service 5. Controllers
  20. 20. # services.yml imports: - { resource: controllers.yml } # controllers.yml ulabox_ulaoffice.controllers.order: class: UlaboxUlaofficeBundleControllerOrderController arguments: - '@refund' - '@router' ...
  21. 21. 5. Controllers 5.3. Dependency Injection
  22. 22. /** * @Route("/orders", service="ulabox_ulaoffice.controllers.order") */ class OrderController { /** * @param Refund $refund * @param RouterInterface $router */ public function __construct(Refund $refund, RouterInterface $router, ……..) { $this->refund = $refund; $this->router = $router; ... } }
  23. 23. 5. Controllers 5.4. Delegate logic to services
  24. 24. /** * @Route("/orders", service="ulabox_ulaoffice.controllers.order") */ class OrderController { public function refundAction(Request $request, $id) { $amount = $request->request->get('refund'); $method = $request->request->get('method'); $orderId = $request->request->get('order_id'); try { $this->refund->execute($orderId, $id, (float)$amount, $method); $this->session->getFlashBag()->add('success', 'Refund has been processed correctly'); } catch (Exception $e) { $this->session->getFlashBag()->add('danger', $e->getMessage()); } return new RedirectResponse($this->router->generate('order_show', ['id' => $orderId])); } }
  25. 25. 5. Controllers 5.5. Unit test
  26. 26. class OrderControllerTest extends PHPUnit_Framework_TestCase { public function setUp() { $this->refund = $this->prophesize(Refund::class); $this->router = $this->prophesize(RouterInterface::class); $this->orderController = new OrderController( $this->refund->reveal(), $this->router->reveal() ); } ... }
  27. 27. class OrderControllerTest extends PHPUnit_Framework_TestCase { ... public function testShouldDelegateOrderRefund() { $orderPaymentId = 34575; $amount = 10.95; $orderId = 12345; $orderRoute = 'some/route'; $request = $this->mockRequest($orderId, $orderPaymentId, $amount, $orderRoute); $this->refund ->execute($orderId, $orderPaymentId, $amount, PaymentPlatform::REDSYS) ->shouldBeCalled(); $this->router->generate('order_show', ['id' => $orderId])->willReturn($orderRoute); $actual = $this->orderController->refundAction($request->reveal(), $orderPaymentId); $this->assertEquals(new RedirectResponse($orderRoute), $actual); } }
  28. 28. 6. Symfony Forms
  29. 29. RESCHEDULE
  30. 30. 6. Symfony Forms 6.1. Anemic Model
  31. 31. class OrderController extends BaseController { public function rescheduleAction(Request $request, $id) { $order = $this->container->get('order')->reposition($id); $form = $this->createForm(new OrderType(), $order); $form->handleRequest($request); if ($form->isValid()) { $em = $this->getDoctrine()->getManager(); $em->persist($order); $em->flush(); $request->getSession()->getFlashBag()->add('success', 'Your changes were saved!'); return $this->redirect($this->generateUrl('reschedule_success')); } return ['entity' => $entity, 'form' => $form->createView()]; } }
  32. 32. Decoupling Ulabox.com monolith. From CRUD to DDD Problems ● Coupling between entities and Symfony Forms. ● Anemic Model. ● Intention?
  33. 33. Decoupling Ulabox.com monolith. From CRUD to DDD Solutions ● Use of DTO/Command ● Reflect the Intention! ● Rich Domain. ● Testing.
  34. 34. 6. Symfony Forms 6.2. Command !== CLI Command
  35. 35. class Reschedule { public $orderId; public $addressId; public $slotVars; public $comments; public function __construct($orderId, $addressId, $slotVars, $comments) { $this->orderId = $orderId; $this->addressId = $addressId; $this->slotVars = $slotVars; $this->comments = $comments; } }
  36. 36. 6. Symfony Forms 6.3. Building the Form
  37. 37. class OrderController extends BaseController { public function rescheduleDisplayingAction(Request $request, $id) { $order = $this->orderRepository->get($id); $address = $order->deliveryAddress()->asAddress(); $rescheduleOrder = Reschedule::fromPayload([ 'order_id' => $order->getId(), 'address_id' => $address->getId(), 'slot_vars' => $order->deliverySlotVars(), 'comments' => $order->deliveryComments(), ]); $rescheduleForm = $this->formFactory->create(OrderRescheduleType::class, $rescheduleOrder); return ['order' => $order, 'form' => $rescheduleForm->createView()]; } }
  38. 38. 6. Symfony Forms 6.4. Submitting the Form
  39. 39. class OrderController extends BaseController { public function rescheduleUpdateAction(Request $request, $id) { $requestData = $request->get('order_reschedule'); $rescheduleOrder = Reschedule::fromPayload([ 'order_id' => $id, 'address_id' => $requestData['addressId'], 'slot_vars' => $requestData['slotVars'], 'comments' => $requestData['comments'], ]); $rescheduleForm = $this->formFactory->create(OrderRescheduleType::class, $rescheduleOrder); if ($rescheduleForm->isValid()) { $this->commandBus->dispatch($rescheduleOrder); } return new RedirectResponse($this->router->generate($this->entity Properties['route'])); } }
  40. 40. 6. Symfony Forms 6.5. Unit test
  41. 41. class OrderControllerTest extends PHPUnit_Framework_TestCase { public function testShouldDelegateOrderRescheduleToCommandBus() { $orderId = 12345; $addressId = 6789; $slotVars = '2016-03-25|523|2|15'; $comments = 'some comments'; $expectedRoute = 'http://some.return.url'; $request = $this->mockRequest($orderId, $addressId, $slotVars, $comments); $form = $this->mockForm(); $form->isValid()->willReturn(true); $this->router->generate('order')->willReturn($expectedRoute); $this->commandBus->dispatch(Argument::type(Reschedule::class))->shouldBeCalled(); $actual = $this->orderController->rescheduleUpdateAction($request->reveal(), $orderId); $this->assertEquals(new RedirectResponse($expectedRoute), $actual); } }
  42. 42. 7. From CRUD to DDD
  43. 43. 7. From CRUD to DDD 7.1. Summing...
  44. 44. class OrderController extends BaseController { public function rescheduleUpdateAction(Request $request, $id) { $requestData = $request->get('order_reschedule'); $rescheduleOrder = Reschedule::fromPayload([ 'order_id' => $id, 'address_id' => $requestData['addressId'], 'slot_vars' => $requestData['slotVars'], 'comments' => $requestData['comments'], ]); $rescheduleForm = $this->formFactory->create(OrderRescheduleType::class, $rescheduleOrder); if ($rescheduleForm>isValid()) { $this->commandBus->dispatch($rescheduleOrder); } return new RedirectResponse($this->router->generate($this->entity Properties['route'])); } }
  45. 45. 7. From CRUD to DDD 7.2. Handling
  46. 46. class RescheduleHandler extends CommandHandler { public function __construct( ... ) { ... } public function handleReschedule(Reschedule $rescheduleOrder) { $timeLineSlot = $this->slotManager->createTimelineSlotFromVars($rescheduleOrder->slotVars); $order = $this->orderRepository->get($rescheduleOrder->aggregateId); $delivery = $order->getOrderDelivery(); $delivery->setSlot($timeLineSlot->getSlot()); $delivery->setLoadTime($timeLineSlot->getLoadTime()); $delivery->setShift($timeLineSlot->getShift()->getShift()); ... $order->rescheduleDelivery($delivery); $this->orderRepository->save($order); $this->eventBus->publish($order->getUncommittedEvents()); } }
  47. 47. Decoupling Ulabox.com monolith. From CRUD to DDD Problems ● Biz logic out of domain. ● Aggregate access. ● Aggregate Root? ● Unprotected Domain.
  48. 48. Decoupling Ulabox.com monolith. From CRUD to DDD Solutions ● Aggregate Root. Order or Delivery? ● Unique acces point to the domain. ● Clear intention!! ● Testing.
  49. 49. 7. From CRUD to DDD 7.3. Order or Delivery?
  50. 50. 7. From CRUD to DDD 7.4. Aggregate access point
  51. 51. class RescheduleHandler extends CommandHandler { public function __construct( ... ) { ... } public function handleReschedule(Reschedule $rescheduleDelivery) { $timeLineSlot = $this->slotManager->createTimelineSlotFromVars($rescheduleDelivery->slotVars); $delivery = $this->deliveryRepository->get($rescheduleDelivery->deliveryId); $delivery->reschedule($timeLineSlot); $this->deliveryRepository->save($delivery); $this->eventBus->publish($delivery->getUncommittedEvents()); } }
  52. 52. 7. From CRUD to DDD 7.5. Business logic
  53. 53. class Delivery implements AggregateRoot { public function reschedule(TimelineSlot $timelineSlot) { $this->setDate($timelineSlot->getDate()); $this->setLoadTime($timelineSlot->getLoadTime()); $this->setSlot($timelineSlot->getSlot()); $this->setShift($timelineSlot->getShift()); $this->setLoad($timelineSlot->getLoad()); $this->setPreparation($timelineSlot->getPreparationDate()); $this->apply( new DeliveryWasRescheduled( $this->getAggregateRootId(), $this->getProgrammedDate(), $this->getTimeStart(), $this->getTimeEnd(), $this->getLoad()->spokeId() ) ); } }
  54. 54. 7. From CRUD to DDD 7.6. Unit test
  55. 55. class RescheduleHandlerTest extends PHPUnit_Framework_TestCase { public function testShouldRescheduleDelivery() { $deliveryId = 12345; $slotVars = '2016-03-25|523|2|15'; $timeLineSlot = TimelineSlotStub::random(); $delivery = $this->prophesize(Delivery::class); $this->deliveryRepository->get($deliveryId)->willReturn($delivery); $this->slotManager->createTimelineSlotFromVars($slotVars)->willReturn($timeLineSlot); $delivery->reschedule($timeLineSlot)->shouldBeCalled(); $this->deliveryRepository->save($delivery)->shouldBeCalled(); $this->eventBus->publish($this->expectedEvents())->shouldBeCalled(); $this->rescheduleOrderHandler->handleReschedule(new Reschedule($deliveryId, $slotVars)); } }
  56. 56. class DeliveryTest extends PHPUnit_Framework_TestCase { public function testShouldRescheduleDelivery() { $delivery = OrderDeliveryStub::random(); $timeLineSlot = TimelineSlotStub::random(); $delivery->reschedule($timeLineSlot); static::assertEquals($timeLineSlot->getDate(), $delivery->getProgrammedDate()); static::assertEquals($timeLineSlot->getLoadTime(), $delivery->getLoadTime()); static::assertEquals($timeLineSlot->getSlot(), $delivery->getSlot()); static::assertEquals($timeLineSlot->getShift(), $delivery->getShift()); static::assertEquals($timeLineSlot->getPreparationDate(), $delivery->getPreparation()); $messageIterator = $delivery->getUncommittedEvents()->getIterator(); $this->assertInstanceOf( DeliveryWasRescheduled::class, $messageIterator->current()->getPayload() ); } }
  57. 57. 7. From CRUD to DDD 7.7. Domain event
  58. 58. DeliveryWasRescheduled Delivery Order Load Slot TimeStart Date OrderLine Product Tax Deliveries Orders
  59. 59. 8. Aggregates and Repositories
  60. 60. CREDIT CARDS
  61. 61. 8. Aggregates and Repositories 8.1. Entity / Repository
  62. 62. class CustomerCreditcardModel { public function add($number, $type, $token = null, $expiryDate = null) { $customer = $this->tokenStorage->getToken()->getUser(); $creditCard = new CustomerCreditcard(); $creditCard->setNumber($number); $creditCard->setCustomer($customer); $creditCard->setType($type); $creditCard->setToken($token); $creditCard->setExpiryDate($expiryDate); $this->creditCardRepository->add($creditCard); return $creditCard; } }
  63. 63. Decoupling Ulabox.com monolith. From CRUD to DDD Problems ● Aggregate? ● CreditCardRepository??? ● Unprotected Domain.
  64. 64. Decoupling Ulabox.com monolith. From CRUD to DDD Solutions ● Which is the Aggregate? ● What’s the Intention? ● Testing
  65. 65. Customer CreditCard class Customer implements AggregateRoot { public function addCreditCard($number, $type, $token = '', $expiryDate = '') { $creditCard = CustomerCreditcard::create($number, $type, $token, $expiryDate); $this->creditCards->add($creditCard); $this->apply(new CreditCardWasRegistered($this->getAggregateRootId(), $number)); } } Decoupling Ulabox.com monolith. From CRUD to DDD
  66. 66. 8. Aggregates and Repositories 8.2. RegisterCreditCard
  67. 67. class RegisterCreditCard { public $customerId; public $cardNumber; public $type; public $token; public $expiry; public function __construct($customerId, $cardNumber, $type, $token, $expiry) { $this->customerId = $customerId; $this->cardNumber = $cardNumber; $this->type = $type; $this->token = $token; $this->expiry = $expiry; } }
  68. 68. 8. Aggregates and Repositories 8.3. RegisterCreditCardHandler
  69. 69. class RegisterCreditCardHandler extends CommandHandler { private $customerRepository; private $eventBus; public function __construct( ... ) { ... } public function handleRegisterCreditCard(RegisterCreditCard $registerCreditCard) { $customer = $this->customerRepository->get($registerCreditCard->customerId()) $customer->addCreditCard( $registerCreditCard->cardNumber(), $registerCreditCard->type(), $registerCreditCard->token(), $registerCreditCard->expiry() ); $this->customerRepository->save($customer); $this->eventBus->publish($customer->getUncommittedEvents()); } }
  70. 70. 8. Aggregates and Repositories 8.4. Business rules
  71. 71. class Customer implements AggregateRoot { public function addCreditCard($number, $type, $token, $expiryDate) { if ($this->creditCardExists($number, $type)) { $this->renewCreditCard($number, $type, $token, $expiryDate); return; } $creditCard = CustomerCreditcard::create($number, $type, $token, $expiryDate); $this->creditCards->add($creditCard); $this->apply(new CreditCardWasRegistered($this->getAggregateRootId(), $number)); } private function renewCreditCard($number, $type, $token, $expiryDate) { ... } }
  72. 72. 9. Learned lessons
  73. 73. This is not a Big-Bang Decoupling Ulabox.com monolith. From CRUD to DDD
  74. 74. Aggregate Election Decoupling Ulabox.com monolith. From CRUD to DDD
  75. 75. Communication Decoupling Ulabox.com monolith. From CRUD to DDD
  76. 76. Team Decoupling Ulabox.com monolith. From CRUD to DDD
  77. 77. ¡¡¡Be a Professional!!! Decoupling Ulabox.com monolith. From CRUD to DDD
  78. 78. @avergess aleix.verges@ulabox.com www.linkedin.com/in/avergess Thank’s Questions?

×