Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Desbravando monolitos modulares

Desbravando monolitos modulares

Palestra apresentada no PHP Summit 2023

Avatar for Mateus Guimarães

Mateus Guimarães

October 09, 2023
Tweet

More Decks by Mateus Guimarães

Other Decks in Programming

Transcript

  1. Módulo • Pequeno • Autocontido • Responsável por um conjunto

    específico de funções • Desenvolvido independentemente • Reutilizável
  2. • Um artefato • Independente de outras aplicações • Um

    banco de dados • Uma unidade de CI/CD • Infraestrutura mais simples • Comunicação intra-processo Monolitos
  3. • Múltiplas aplicações, entregues separadamente • Integração entre diferentes aplicações

    • Múltiplos bancos de dados • Escalável independentemente • Comunicação via rede • Limites entre serviços bem estabelecidos Micro-serviços
  4. A simplicidade de um monolito Monolito Micro-serviços • Um repositório

    • Infraestrutura simples • Mais simples de ser testado • Transações de banco nativas • Chamadas locais • Múltiplos repositórios • Infraestrutura mais complexa • Testes de integração complexos • Transações orquestradas • Chamadas de rede
  5. • Módulos mal-definidos e limites (boundaries) não estabelecidos • Alto

    nível de acoplamento • Alto esforço cognitivo para entender o projeto • Dificuldade de onboarding • Uma pequena mudança numa funcionalidade quebra outras Big Ball of Mud
  6. readonly class User { public function __ construct( public int

    $id, public string $email ) {} } class TokenIssuer { public function issue(int $uniqueIdentifier): Token { // some magic return new Token($payload); } } $user = new App\User('Mateus', '[email protected]'); $token = (new App\Auth\TokenIssuer) -> issue($user -> id);
  7. <?php class PaymentService { public function __ construct( protected PaymentGateway

    $gateway ) {} public function charge(int $orderId, Dollars $amount, PaymentToken $token): Payment { $charge = $this -> gateway - > charge($token, $amount); return Payment::createFromCharge($charge, $orderId); } } // ... class PayOrder { public function handle(Order $order, PaymentToken $token): Payment { $this - > paymentService -> charge($order - > id, $order -> total, $token); } }
  8. • Fácil de entender • Excelente para aplicações pequenas •

    Dificuldade de manter coesão conforme uma aplicação cresce • Dificuldade de estabelecer limites entre módulos Grupamento por tipo
  9. • Separação lógica entre módulos • Facilidade de estabelecer limites

    entre módulos • Clareza acerca das responsabilidades de um módulo • Possibilidade de implementar estratégias diferentes para módulos com desafios diferentes • Adoção gradual Estrutura Modular
  10. Comunicação via contratos • Módulos podem ser desenvolvidos de forma

    isolada • Mudanças na implementação não afetam outros módulos, desde que os contratos sejam respeitados • Contratos do produtor podem ser redirecionados para outras implementações sem conhecimento do consumidor • Pode-se substituir as “bordas" de módulos upstream por implementações falsas (por exemplo, em memória) durante testes
  11. Data Transfer Objects • Serve como um “recipiente" para mandar

    mensagens de um módulo para o outro (e internamente, também) • Sem comportamento • Imutável • Plain Old PHP Object (POP)
  12. class CheckoutController { public function __ invoke(CheckoutRequest $request): JsonResponse {

    $items = $request -> items(); $total = $this -> calculateTotal($items); $order = DB::transaction(function () use ($request, $total) { $order = $request -> user() -> orders() -> make([ 'total' => $total ]); foreach ($items as $item) { $order -> addLine($item -> product, $item -> price, $item -> quantity); $item -> product -> decreaseStock($item -> quantity); } $order -> save(); $charge = $this -> paymentGateway -> charge($request -> token(), $total); $this -> createPaymentForOrder($order, $charge); $order -> prepareForShipment(); return $order; }); return new OrderResource($order); } }
  13. class CheckoutController { public function __ invoke(CheckoutRequest $request): JsonResponse {

    $items = $request -> items(); $total = $this -> calculateTotal($items); $order = DB::transaction(function () use ($request, $total) { $order = $request -> user() -> orders() -> make([ 'total' => $total ]); foreach ($items as $item) { $order -> addLine($item -> product, $item -> price, $item -> quantity); $item -> product -> decreaseStock($item -> quantity); } $order -> save(); $charge = $this -> paymentGateway -> charge($request -> token(), $total); $this -> createPaymentForOrder($order, $charge); $order -> prepareForShipment(); return $order; }); return new OrderResource($order); } }
  14. class CheckoutController { public function __ invoke(CheckoutRequest $request): JsonResponse {

    $items = $request -> items(); $total = $this -> calculateTotal($items); $order = DB::transaction(function () use ($request, $total) { $order = $request -> user() -> orders() -> make([ 'total' => $total ]); foreach ($items as $item) { $order -> addLine($item -> product, $item -> price, $item -> quantity); $item -> product -> decreaseStock($item -> quantity); } $order -> save(); $charge = $this -> paymentGateway -> charge($request -> token(), $total); $this -> createPaymentForOrder($order, $charge); $order -> prepareForShipment(); return $order; }); return new OrderResource($order); } }
  15. class CheckoutController { public function __ invoke(CheckoutRequest $request): JsonResponse {

    $items = $request -> items(); $total = $this -> calculateTotal($items); $order = DB::transaction(function () use ($request, $total) { $order = $request -> user() -> orders() -> make([ 'total' => $total ]); foreach ($items as $item) { $order -> addLine($item -> product, $item -> price, $item -> quantity); $item -> product -> decreaseStock($item -> quantity); } $order -> save(); $charge = $this -> paymentGateway -> charge($request -> token(), $total); $this -> createPaymentForOrder($order, $charge); $order -> prepareForShipment(); return $order; }); return new OrderResource($order); } }
  16. class CheckoutController { public function __ invoke(CheckoutRequest $request): JsonResponse {

    $items = $request -> items(); $total = $this -> calculateTotal($items); $order = DB::transaction(function () use ($request, $total) { $order = $request -> user() -> orders() -> make([ 'total' => $total ]); foreach ($items as $item) { $order -> addLine($item -> product, $item -> price, $item -> quantity); $item -> product -> decreaseStock($item -> quantity); } $order -> save(); $charge = $this -> paymentGateway -> charge($request -> token(), $total); $this -> createPaymentForOrder($order, $charge); $order -> prepareForShipment(); return $order; }); return new OrderResource($order); } }
  17. class CheckoutController { public function __ invoke(CheckoutRequest $request): JsonResponse {

    $items = $request -> items(); $total = $this -> calculateTotal($items); $order = DB::transaction(function () use ($request, $total) { $order = $request -> user() -> orders() -> make([ 'total' => $total ]); foreach ($items as $item) { $order -> addLine($item -> product, $item -> price, $item -> quantity); $item -> product -> decreaseStock($item -> quantity); } $order -> save(); $charge = $this -> paymentGateway -> charge($request -> token(), $total); $this -> createPaymentForOrder($order, $charge); $order -> prepareForShipment(); return $order; }); return new OrderResource($order); } }
  18. class CheckoutController { public function __ invoke(CheckoutRequest $request): JsonResponse {

    $items = $request -> items(); $total = $this -> calculateTotal($items); $order = DB::transaction(function () use ($request, $total) { $order = $request -> user() -> orders() -> make([ 'total' => $total ]); foreach ($items as $item) { $order -> addLine($item -> product, $item -> price, $item -> quantity); $item -> product -> decreaseStock($item -> quantity); } $order -> save(); $charge = $this -> paymentGateway -> charge($request -> token(), $total); $this -> createPaymentForOrder($order, $charge); $order -> prepareForShipment(); return $order; }); return new OrderResource($order); } }
  19. use Modules\Checkout\PurchaseItems; use Modules\Payment\PaymentGateway; use Modules\Product\FetchCartItemsByProductIds; class CheckoutController { public

    function __ construct( protected FetchCartItemsByProductIds $fetchCartItems, protected PurchaseItems $purchaseItems, protected PaymentGateway $paymentGateway ) {} public function __ invoke(CheckoutRequest $request): JsonResponse { $items = $this-fetchCartItemsByProductIds -> fromCheckoutRequest($request); $this -> paymentGateway -> setToken($request -> paymentToken()); try { $order = $this -> purchaseItems -> handle($items, $this -> paymentGateway, $request -> user() -> id); } catch (ProductsOutOfStockException | PaymentFailedException) { } return new OrderResource($order); } }
  20. class PurchaseItems { public function __ construct( protected Database $database,

    ) {} public function handle(CartItemCollection $items, PaymentGateway $paymentGateway, int $userId): Order { $order = $this - > database -> transaction(function () use ($items, $paymentGateway, $userId) { $order = Order::start($userId); $order -> addLinesFromCartItems($items); $order -> save(); // Para termos um ID. Um uuid serviria. foreach ($items as $item) { $item - > product - > decrement('stock', $item -> quantity); } $charge = $paymentGateway -> charge($order - > total); $payment = Payment::create([...]); $order -> complete(); Shipment::create([...]); return $order; }); return $order; } }
  21. class PurchaseItems { public function __ construct( protected Database $database,

    ) {} public function handle(CartItemCollection $items, PaymentGateway $paymentGateway, int $userId): Order { $order = $this - > database -> transaction(function () use ($items, $paymentGateway, $userId) { $order = Order::start($userId); $order -> addLinesFromCartItems($items); $order -> save(); // Para termos um ID. Um uuid serviria. foreach ($items as $item) { $item - > product - > decrement('stock', $item -> quantity); } $charge = $paymentGateway -> charge($order - > total); $payment = Payment::create([...]); $order -> complete(); Shipment::create([...]); return $order; }); return $order; } }
  22. class PurchaseItems { public function __ construct( protected Database $database,

    ) {} public function handle(CartItemCollection $items, PaymentGateway $paymentGateway, int $userId): Order { $order = $this - > database -> transaction(function () use ($items, $paymentGateway, $userId) { $order = Order::start($userId); $order -> addLinesFromCartItems($items); $order -> save(); // Para termos um ID. Um uuid serviria. foreach ($items as $item) { $item - > product - > decrement('stock', $item -> quantity); } $charge = $paymentGateway -> charge($order - > total); $payment = Payment::create([...]); $order -> complete(); Shipment::create([...]); return $order; }); return $order; } }
  23. class PurchaseItems { public function __ construct( protected Database $database,

    ) {} public function handle(CartItemCollection $items, PaymentGateway $paymentGateway, int $userId): Order { $order = $this - > database -> transaction(function () use ($items, $paymentGateway, $userId) { $order = Order::start($userId); $order -> addLinesFromCartItems($items); $order -> save(); // Para termos um ID. Um uuid serviria. foreach ($items as $item) { $item - > product - > decrement('stock', $item -> quantity); } $charge = $paymentGateway -> charge($order - > total); $payment = Payment::create([...]); $order -> complete(); Shipment::create([...]); return $order; }); return $order; } }
  24. class PurchaseItems { public function __ construct( protected Database $database,

    ) {} public function handle(CartItemCollection $items, PaymentGateway $paymentGateway, int $userId): Order { $order = $this - > database -> transaction(function () use ($items, $paymentGateway, $userId) { $order = Order::start($userId); $order -> addLinesFromCartItems($items); $order -> save(); // Para termos um ID. Um uuid serviria. foreach ($items as $item) { $item - > product - > decrement('stock', $item -> quantity); } $charge = $paymentGateway -> charge($order - > total); $payment = Payment::create([...]); $order -> complete(); Shipment::create([...]); return $order; }); return $order; } }
  25. class PurchaseItems { public function __ construct( protected Database $database,

    protected InventoryService $inventoryService, protected PaymentService $paymentService, protected ShipmentService $shipmentService, ) {} public function handle(CartItemCollection $items, PaymentGateway $paymentGateway, int $userId): OrderDto { $order = $this -> database -> transaction(function () use ($items, $paymentGateway, $userId) { $order = Order::start($userId); $order -> addLinesFromCartItems($items); $order -> save(); // Para termos um ID. Um uuid serviria. foreach ($items as $item) { $this -> inventoryService -> decreaseStock($item - > productId, $item -> quantity); } /** @var PaymentDto $payment */ $payment = $this -> paymentService - > payOrder($order -> toDto(), $paymentGateway); $order -> complete(); $this -> shipmentService - > startShipmentForOrder($order - > toDto()); return $order -> toDto(); }); return $order; } }
  26. Comunicação via eventos • Maior desacoplamento entre módulos • Maior

    extensibilidade • Maior tolerância à falhas • Consistência eventual • Necessidade de ações compensatórias • Maior complexidade
  27. class PurchaseItems { public function __ construct( protected Bus $events,

    ) {} public function handle(CartItemCollection $items, PaymentGateway $paymentGateway, int $userId): OrderDto { $order = Order::start($userId); $order -> addLinesFromCartItems($items); $order -> initiate(); $this -> events -> dispatch( new OrderInitiated( order: $order -> toDto(), PaymentGateway: $paymentGateway, ) ); return $order0 -> toDto(); } }
  28. class PayOrder { public function __ construct( protected Bus $events,

    protected PaymentService $paymentService, ) {} public function handle(OrderInitiated $event): void { try { $payment = $this -> paymentService -> payOrder($event - > order, $event -> paymentGateway); } catch (PaymentFailedException $exception) { $this -> events - > dispatch( new PaymentFailed( orderId: $event -> order - > id, reason: $exception -> getMessage(), ) ); return; } $this - > events - > dispatch( new PaymentSucceeded( orderId: $event - > order -> id, payment: $payment - > toDto(), ) ); } }
  29. class FulfillOrder { public function handle(PaymentSucceeded $event): void { $order

    = $this - > getOrderSomehow($event -> order -> id); $order - > fulfill(); Mail::to($order -> user -> email) -> send(new OrderCompleted($order)); } } class StartShipmentForOrder { public function __ construct( protected ShipmentService $shipmentService ) public function handle(PaymentSucceeded $event): void { $this -> shipmentService - > startShipmentForOrder($event -> order -> id); } } class DecreaseProductStock { public function __ construct( protected InventoryService $inventoryService ) {} public function handle(PaymentSucceeded $event): void { foreach ($event -> order - > lines as $orderLine) { $this -> inventoryService - > decreaseStockForProduct($orderLine -> productId, $orderLine - > quantity); } } }
  30. namespace Modules\Order; class Order { public function __ construct( public

    int $id, public Money $total, /** @var OrderLine[] */ public array $lines, public OrderStatus $status, public Payment $payment, public int $userId, ) {} }
  31. namespace Modules\Accounting; readonly class Sale { public function __ construct(

    public int $id, public Money $total, public Money $net, public Money $tax, public TaxBracket $taxBracket, public Address $address, public Calculation $calculation, public TaxReceipt $receipt, public BalanceSheet $balance, public Uuid $uuid, public int $orderId, ) {} }
  32. namespace Modules\Accounting; readonly class Sale { public function __ construct(

    public int $id, public Money $total, public Money $net, public Money $tax, public TaxBracket $taxBracket, public Address $address, public Calculation $calculation, public TaxReceipt $receipt, public BalanceSheet $balance, public Uuid $uuid, public int $orderId, ) {} }
  33. namespace Modules\Accounting; readonly class Sale { public function __ construct(

    public int $id, public Money $total, public Money $net, public Money $tax, public TaxBracket $taxBracket, public Address $address, public Calculation $calculation, public TaxReceipt $receipt, public BalanceSheet $balance, public Uuid $uuid, public int $orderId, ) {} }
  34. Sugestões • Acoplamento intra-módulo é “menos preocupante” do que acoplamento

    entre módulos. • Exponha uma API *pequena*, bem definida e estável para que outros módulos tenham acesso. • Ao se comunicar com outros módulos, sempre dependa da API pública daquele módulo, e jamais de implementações. • Um dos maiores benefícios de arquiteturas modulares, na minha opinião, é poder implementar estratégias diferentes para problemas diferentes. Use isto. • Cuidado com acoplamento à nível de banco de dados, como com chaves estrangeiras. • De forma geral, queries dentro de um módulo devem tocar apenas tabelas pertencentes aquele módulo. • Use o Deptrac! (Em PHP)
  35. Mantendo módulos consistentes Análise estática via Deptrac • O Deptrac

    permite configurar “camadas”, e também criar regras sobre como essas camadas interagem com outras — incluindo com quais camadas elas podem ou não podem interagir • O Deptrac também gera um grafo de dependências, inclusive mostrando onde as violações das regras acontecem (caso haja alguma)