Top Banner
Decoupling Ulabox.com monolith From CRUD to DDD
83

Decoupling the Ulabox.com monolith. From CRUD to DDD

Apr 16, 2017

Download

Software

Aleix Vergés
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: Decoupling the Ulabox.com monolith. From CRUD to DDD

Decoupling Ulabox.com monolith

From CRUD to DDD

Page 2: Decoupling the Ulabox.com monolith. From CRUD to DDD

Aleix VergésBackend developer

Scrum Master

@avergess

[email protected]

Page 3: Decoupling the Ulabox.com monolith. From CRUD to DDD

1. What’s Ulabox?

Page 4: Decoupling the Ulabox.com monolith. From CRUD to DDD
Page 5: Decoupling the Ulabox.com monolith. From CRUD to DDD
Page 6: Decoupling the Ulabox.com monolith. From CRUD to DDD
Page 7: Decoupling the Ulabox.com monolith. From CRUD to DDD
Page 8: Decoupling the Ulabox.com monolith. From CRUD to DDD

2. Decoupling. Why?

Page 9: Decoupling the Ulabox.com monolith. From CRUD to DDD

/*** @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

Page 10: Decoupling the Ulabox.com monolith. From CRUD to DDD

WTF!

Page 11: Decoupling the Ulabox.com monolith. From CRUD to DDD

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

Page 12: Decoupling the Ulabox.com monolith. From CRUD to DDD

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

subs

cribe

subscribe

subscribe

subscribesubscribe

subscribe

Decoupling Ulabox.com monolith. From CRUD to DDD

Page 13: Decoupling the Ulabox.com monolith. From CRUD to DDD

3. The tools

Page 14: Decoupling the Ulabox.com monolith. From CRUD to DDD

● 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

Page 15: Decoupling the Ulabox.com monolith. From CRUD to DDD

4. A responsability question

Page 16: Decoupling the Ulabox.com monolith. From CRUD to DDD

Refactoring and manage technical debt is not a choice, but a responsability

Decoupling Ulabox.com monolith. From CRUD to DDD

Page 17: Decoupling the Ulabox.com monolith. From CRUD to DDD

5. Controllers

Page 18: Decoupling the Ulabox.com monolith. From CRUD to DDD

REFUND!

Page 19: Decoupling the Ulabox.com monolith. From CRUD to DDD

5.1. OrderController5. Controllers

Page 20: Decoupling the Ulabox.com monolith. From CRUD to DDD

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

}

Page 21: Decoupling the Ulabox.com monolith. From CRUD to DDD

Decoupling Ulabox.com monolith. From CRUD to DDD

Problems● Hidden dependencies

● Inheritance.

● Biz logic in the controller.

● Non aggregate root.

● Difficult to test.

Page 22: Decoupling the Ulabox.com monolith. From CRUD to DDD

* 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

Page 23: Decoupling the Ulabox.com monolith. From CRUD to DDD

5.2. Controller as a service5. Controllers

Page 24: Decoupling the Ulabox.com monolith. From CRUD to DDD

# services.yml

imports: - { resource: controllers.yml }

# controllers.yml

ulabox_ulaoffice.controllers.order: class: Ulabox\UlaofficeBundle\Controller\OrderController arguments: - '@refund' - '@router' ...

Page 25: Decoupling the Ulabox.com monolith. From CRUD to DDD

5. Controllers

5.3. Dependency Injection

Page 26: Decoupling the Ulabox.com monolith. From CRUD to DDD

/*** @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; ... }}

Page 27: Decoupling the Ulabox.com monolith. From CRUD to DDD

5. Controllers

5.4. Delegate logic to services

Page 28: Decoupling the Ulabox.com monolith. From CRUD to DDD

/*** @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]));}

}

Page 29: Decoupling the Ulabox.com monolith. From CRUD to DDD

5. Controllers

5.5. Unit test

Page 30: Decoupling the Ulabox.com monolith. From CRUD to DDD

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

);}

...}

Page 31: Decoupling the Ulabox.com monolith. From CRUD to DDD

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

}

Page 32: Decoupling the Ulabox.com monolith. From CRUD to DDD

6. Symfony Forms

Page 33: Decoupling the Ulabox.com monolith. From CRUD to DDD

RESCHEDULE

Page 34: Decoupling the Ulabox.com monolith. From CRUD to DDD

6. Symfony Forms

6.1. Anemic Model

Page 35: Decoupling the Ulabox.com monolith. From CRUD to DDD

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

}

Page 36: Decoupling the Ulabox.com monolith. From CRUD to DDD

Decoupling Ulabox.com monolith. From CRUD to DDD

Problems● Coupling between entities and Symfony Forms.

● Anemic Model.

● Intention?

Page 37: Decoupling the Ulabox.com monolith. From CRUD to DDD

Decoupling Ulabox.com monolith. From CRUD to DDD

Solutions● Use of DTO/Command

● Reflect the Intention!

● Rich Domain.

● Testing.

Page 38: Decoupling the Ulabox.com monolith. From CRUD to DDD

6. Symfony Forms

6.2. Command !== CLI Command

Page 39: Decoupling the Ulabox.com monolith. From CRUD to DDD

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

Page 40: Decoupling the Ulabox.com monolith. From CRUD to DDD

6. Symfony Forms

6.3. Building the Form

Page 41: Decoupling the Ulabox.com monolith. From CRUD to DDD

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

}

Page 42: Decoupling the Ulabox.com monolith. From CRUD to DDD

6. Symfony Forms

6.4. Submitting the Form

Page 43: Decoupling the Ulabox.com monolith. From CRUD to DDD

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

Page 44: Decoupling the Ulabox.com monolith. From CRUD to DDD

6. Symfony Forms

6.5. Unit test

Page 45: Decoupling the Ulabox.com monolith. From CRUD to DDD

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

}}

Page 46: Decoupling the Ulabox.com monolith. From CRUD to DDD

7. From CRUD to DDD

Page 47: Decoupling the Ulabox.com monolith. From CRUD to DDD

7. From CRUD to DDD

7.1. Summing...

Page 48: Decoupling the Ulabox.com monolith. From CRUD to DDD

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

Page 49: Decoupling the Ulabox.com monolith. From CRUD to DDD

7. From CRUD to DDD

7.2. Handling

Page 50: Decoupling the Ulabox.com monolith. From CRUD to DDD

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

}}

Page 51: Decoupling the Ulabox.com monolith. From CRUD to DDD

Decoupling Ulabox.com monolith. From CRUD to DDD

Problems● Biz logic out of domain.

● Aggregate access.

● Aggregate Root?

● Unprotected Domain.

Page 52: Decoupling the Ulabox.com monolith. From CRUD to DDD

Decoupling Ulabox.com monolith. From CRUD to DDD

Solutions● Aggregate Root. Order or Delivery?

● Unique acces point to the domain.

● Clear intention!!

● Testing.

Page 53: Decoupling the Ulabox.com monolith. From CRUD to DDD

7. From CRUD to DDD

7.3. Order or Delivery?

Page 54: Decoupling the Ulabox.com monolith. From CRUD to DDD
Page 55: Decoupling the Ulabox.com monolith. From CRUD to DDD

7. From CRUD to DDD

7.4. Aggregate access point

Page 56: Decoupling the Ulabox.com monolith. From CRUD to DDD

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

Page 57: Decoupling the Ulabox.com monolith. From CRUD to DDD

7. From CRUD to DDD

7.5. Business logic

Page 58: Decoupling the Ulabox.com monolith. From CRUD to DDD

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

Page 59: Decoupling the Ulabox.com monolith. From CRUD to DDD

7. From CRUD to DDD

7.6. Unit test

Page 60: Decoupling the Ulabox.com monolith. From CRUD to DDD

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

}

Page 61: Decoupling the Ulabox.com monolith. From CRUD to DDD

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

} }

Page 62: Decoupling the Ulabox.com monolith. From CRUD to DDD

7. From CRUD to DDD

7.7. Domain event

Page 63: Decoupling the Ulabox.com monolith. From CRUD to DDD

DeliveryWasRescheduled

Delivery Order

Load

Slot

TimeStart

DateOrderLine

Product

Tax

Deliveries Orders

Page 64: Decoupling the Ulabox.com monolith. From CRUD to DDD

8. Aggregates and Repositories

Page 65: Decoupling the Ulabox.com monolith. From CRUD to DDD

CREDIT CARDS

Page 66: Decoupling the Ulabox.com monolith. From CRUD to DDD

8. Aggregates and Repositories

8.1. Entity / Repository

Page 67: Decoupling the Ulabox.com monolith. From CRUD to DDD

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

Page 68: Decoupling the Ulabox.com monolith. From CRUD to DDD

Decoupling Ulabox.com monolith. From CRUD to DDD

Problems● Aggregate?

● CreditCardRepository???

● Unprotected Domain.

Page 69: Decoupling the Ulabox.com monolith. From CRUD to DDD

Decoupling Ulabox.com monolith. From CRUD to DDD

Solutions● Which is the Aggregate?

● What’s the Intention?

● Testing

Page 70: Decoupling the Ulabox.com monolith. From CRUD to DDD

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

Page 71: Decoupling the Ulabox.com monolith. From CRUD to DDD

8. Aggregates and Repositories

8.2. RegisterCreditCard

Page 72: Decoupling the Ulabox.com monolith. From CRUD to DDD

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

Page 73: Decoupling the Ulabox.com monolith. From CRUD to DDD

8. Aggregates and Repositories

8.3. RegisterCreditCardHandler

Page 74: Decoupling the Ulabox.com monolith. From CRUD to DDD

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

}}

Page 75: Decoupling the Ulabox.com monolith. From CRUD to DDD

8. Aggregates and Repositories

8.4. Business rules

Page 76: Decoupling the Ulabox.com monolith. From CRUD to DDD

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) { ... }}

Page 77: Decoupling the Ulabox.com monolith. From CRUD to DDD

9. Learned lessons

Page 78: Decoupling the Ulabox.com monolith. From CRUD to DDD

This is not a Big-Bang

Decoupling Ulabox.com monolith. From CRUD to DDD

Page 79: Decoupling the Ulabox.com monolith. From CRUD to DDD

Aggregate Election

Decoupling Ulabox.com monolith. From CRUD to DDD

Page 80: Decoupling the Ulabox.com monolith. From CRUD to DDD

Communication

Decoupling Ulabox.com monolith. From CRUD to DDD

Page 81: Decoupling the Ulabox.com monolith. From CRUD to DDD

TeamDecoupling Ulabox.com monolith. From CRUD to DDD

Page 82: Decoupling the Ulabox.com monolith. From CRUD to DDD

¡¡¡Be a Professional!!!Decoupling Ulabox.com monolith. From CRUD to DDD