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

Desbravando monolitos modulares

Desbravando monolitos modulares

Palestra apresentada no PHP Summit 2023

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)