Decoupling the Ulabox.com monolith. From CRUD to DDD

Post on 16-Apr-2017

818 views 3 download

Transcript of Decoupling the Ulabox.com monolith. From CRUD to DDD

Decoupling Ulabox.com monolith

From CRUD to DDD

Aleix VergésBackend developer

Scrum Master

@avergess

aleix.verges@ulabox.com

1. What’s Ulabox?

2. Decoupling. Why?

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

WTF!

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

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

3. The tools

● 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

4. A responsability question

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

Decoupling Ulabox.com monolith. From CRUD to DDD

5. Controllers

REFUND!

5.1. OrderController5. Controllers

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

}

Decoupling Ulabox.com monolith. From CRUD to DDD

Problems● Hidden dependencies

● Inheritance.

● Biz logic in the controller.

● Non aggregate root.

● Difficult to test.

* 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

5.2. Controller as a service5. Controllers

# services.yml

imports: - { resource: controllers.yml }

# controllers.yml

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

5. Controllers

5.3. Dependency Injection

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

5. Controllers

5.4. Delegate logic to services

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

}

5. Controllers

5.5. Unit test

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

);}

...}

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

}

6. Symfony Forms

RESCHEDULE

6. Symfony Forms

6.1. Anemic Model

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

}

Decoupling Ulabox.com monolith. From CRUD to DDD

Problems● Coupling between entities and Symfony Forms.

● Anemic Model.

● Intention?

Decoupling Ulabox.com monolith. From CRUD to DDD

Solutions● Use of DTO/Command

● Reflect the Intention!

● Rich Domain.

● Testing.

6. Symfony Forms

6.2. Command !== CLI Command

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

6. Symfony Forms

6.3. Building the Form

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

}

6. Symfony Forms

6.4. Submitting the Form

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

6. Symfony Forms

6.5. Unit test

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

}}

7. From CRUD to DDD

7. From CRUD to DDD

7.1. Summing...

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

7. From CRUD to DDD

7.2. Handling

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

}}

Decoupling Ulabox.com monolith. From CRUD to DDD

Problems● Biz logic out of domain.

● Aggregate access.

● Aggregate Root?

● Unprotected Domain.

Decoupling Ulabox.com monolith. From CRUD to DDD

Solutions● Aggregate Root. Order or Delivery?

● Unique acces point to the domain.

● Clear intention!!

● Testing.

7. From CRUD to DDD

7.3. Order or Delivery?

7. From CRUD to DDD

7.4. Aggregate access point

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

7. From CRUD to DDD

7.5. Business logic

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

7. From CRUD to DDD

7.6. Unit test

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

}

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

} }

7. From CRUD to DDD

7.7. Domain event

DeliveryWasRescheduled

Delivery Order

Load

Slot

TimeStart

DateOrderLine

Product

Tax

Deliveries Orders

8. Aggregates and Repositories

CREDIT CARDS

8. Aggregates and Repositories

8.1. Entity / Repository

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

Decoupling Ulabox.com monolith. From CRUD to DDD

Problems● Aggregate?

● CreditCardRepository???

● Unprotected Domain.

Decoupling Ulabox.com monolith. From CRUD to DDD

Solutions● Which is the Aggregate?

● What’s the Intention?

● Testing

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

8. Aggregates and Repositories

8.2. RegisterCreditCard

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

8. Aggregates and Repositories

8.3. RegisterCreditCardHandler

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

}}

8. Aggregates and Repositories

8.4. Business rules

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

9. Learned lessons

This is not a Big-Bang

Decoupling Ulabox.com monolith. From CRUD to DDD

Aggregate Election

Decoupling Ulabox.com monolith. From CRUD to DDD

Communication

Decoupling Ulabox.com monolith. From CRUD to DDD

TeamDecoupling Ulabox.com monolith. From CRUD to DDD

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

@avergessaleix.verges@ulabox.com

www.linkedin.com/in/avergess

Thank’sQuestions?