Date post: | 16-Apr-2017 |
Category: |
Software |
Upload: | aleix-verges |
View: | 818 times |
Download: | 3 times |
Decoupling Ulabox.com monolith
From CRUD to DDD
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
www.linkedin.com/in/avergess
Thank’sQuestions?