Se você já trabalhou ou trabalha em startups, em um fluxo rápido de desenvolvimento de produto ou até mesmo criando o seu produto, deve saber que um produto pode e vai mudar em algum momento, devido à necessidade de novos investidores ou clientes, ou à decisão de algum stakeholder. Caso você não tenha experiência ou esteja começando agora nesse mundo de entregas rápidas, neste artigo eu pretendo te passar uma breve dica que provavelmente vai te economizar tempo de trabalho e reduzir as suas chances de ter dor de cabeça.
Sumário
- Por que trocar de provedor
- O provedor dentro do domínio
- A modelagem acoplada
- O código acoplado
- E se o provedor mudar?
- A solução
- O domínio fala a própria língua
- Um tradutor para cada provedor
- Juntando tudo
Por que trocar de provedor
Acredito que o cenário mais comum e prático que eu possa dar para ilustrar o que eu quero te mostrar seja o caso em que você precise lidar com a mudança de um provedor de pagamento. Muitas coisas podem fazer com que uma empresa ou um produto cogite mudar o provedor, como: melhores taxas entre provedores, qualidade no suporte, maturidade do provedor e confiabilidade. Até mesmo o cenário em que o dono do provedor de pagamento é conhecido ou amigo da startup ou empresa em que você trabalha pode motivar essa mudança.
O provedor dentro do domínio
Imagine que chega a task para você fazer a implementação de um provedor de pagamento X dentro de um novo produto da empresa. Com prazo apertado e um único provedor, modelar tudo em cima dele parece o caminho mais rápido. Você faz a modelagem, faz a integração, tudo certinho: testado e funcionando. Mas aí, depois de alguns meses, chega a task para implementar um novo provedor de pagamento, e você percebe que vai ser uma dor de cabeça, porque tanto a modelagem quanto o código do domínio foram feitos com o vocabulário do antigo provedor, tornando tudo acoplado. Essa é uma situação que não é nada interessante de se resolver, mas é bem previsível de se combater.
A modelagem acoplada
O código acoplado
Aqui foi exemplificado o impacto na modelagem, mas, além dele, o código também teria outros problemas, como a repetição de código e inúmeras verificações.
final class ReleaseProduct { public function __invoke(Order $order): void { $isPaid = match ($order->provider) { 'stripe' => $order->stripe_status === 'succeeded', 'mercadopago' => $order->mercadopago_status === 'approved', }; if (! $isPaid) { return; } $order->product->release(); } } final class RefundOrder { public function __invoke(Order $order): void { match ($order->provider) { 'stripe' => $this->stripe->refund($order->stripe_payment_intent_id), 'mercadopago' => $this->mercadoPago->refund($order->mercadopago_payment_id), }; } } E se o provedor mudar?
Eu tive o prazer de ter uma ótima liderança técnica e experiente que, antes mesmo de me deixar colocar a mão na modelagem, me mostrou as seguintes questões: "E se o provedor mudar, o que fazer? Qual a consequência de mudar do provedor X para o Y?" Sinceramente, no começo eu não fazia ideia do que fazer. Foi aí que me foi apresentada a ideia de fazer uma modelagem e um código agnóstico de provedor (provider-agnostic).
Na computação, um dispositivo ou programa de software é considerado agnóstico ou agnóstico de dados se o método ou formato de transmissão de dados for irrelevante para a função do dispositivo ou programa. Isso significa que o dispositivo ou programa pode receber dados em múltiplos formatos ou de múltiplas fontes e ainda assim processá-los de forma eficaz. — Wikipédia, tradução livre
Uma maneira prática de absorver essa ideia, e que me ajudou bastante, foi pensar: caso determinada informação possa ter um caminho diferente, tanto a modelagem quanto o código devem se comportar da mesma maneira, independentemente do caminho. No nosso exemplo, a informação é "o pagamento foi concluído", e os caminhos são o Stripe e o Mercado Pago.
Pagamento é só o exemplo que escolhi, mas a mesma ideia aparece em vários outros lugares: provedor de autenticação (Auth0, Cognito, Firebase Auth), envio de e-mail (SES, SendGrid, Mailgun), armazenamento de arquivos (S3, Google Cloud Storage) e envio de SMS (Twilio, Zenvia). Se você usa Laravel, já usa isso no dia a dia: o Mail e o Storage funcionam do mesmo jeito, seja qual for o driver configurado por trás.
A solução
O domínio fala a própria língua
Para tornar o código e a modelagem agnósticos de provedor, você vai precisar entender o que é domínio. Talvez você já tenha ouvido falar sobre DDD; é dele que vem esse termo. Não vou entrar no tema por completo (recomendo que você leia sobre o assunto depois), mas vou explicar brevemente o que seria esse tal domínio.
Entenda o domínio como a área de conhecimento e as regras do problema que o seu produto resolve. No código, ele vira a parte central, que reúne essas regras e entende como resolver o problema. No exemplo que estou utilizando, a nossa área de conhecimento e as regras são de pedido, então o nosso domínio tem tudo o que é necessário para resolver isso.
Agora que sabemos disso, você já deve ter notado que cada provedor tem uma nomenclatura para as informações de que ele precisa ou que retorna. Um pagamento concluído, por exemplo, é succeeded no Stripe e approved no Mercado Pago. Já o nosso domínio usa os próprios nomes, paid ou canceled, escolhidos por você ou pelo seu time, e os nomes dos provedores nunca chegam até ele.
Olhando para a modelagem: a tabela orders guarda só o vocabulário do domínio. O provedor vira apenas um dado (provider), e o id dele fica em uma coluna genérica (provider_reference).
Schema::create('orders', function (Blueprint $table) { $table->id(); $table->integer('amount_cents'); $table->string('status'); $table->string('provider'); $table->string('provider_reference')->nullable(); $table->timestamps(); }); enum PaymentStatus: string { case Pending = 'pending'; case Paid = 'paid'; case Canceled = 'canceled'; } enum PaymentProvider: string { case Stripe = 'stripe'; case MercadoPago = 'mercadopago'; } final class Order extends Model { protected function casts(): array { return [ 'status' => PaymentStatus::class, 'provider' => PaymentProvider::class, ]; } } Um tradutor para cada provedor
O próximo passo é lidar com essa diferença de nomenclatura entre os provedores. Para isso, vamos criar uma classe que faz a tradução da nomenclatura da API para o que temos no nosso domínio. Esse tradutor é um adaptador (adapter), e todos os adaptadores seguem o mesmo contrato, a porta (port) que o domínio define. Essa camada de tradução, como um todo, é o que o DDD chama de camada anticorrupção (Anti-Corruption Layer).
DOMÍNIO (fala "paid", "canceled") ┌─────────────────────────────────────────────────────┐ │ ReleaseProduct RefundOrder HandleWebhook │ └──────────────────────────┬──────────────────────────┘ │ depende só do contrato ┌───────────▼────────────┐ │ PaymentGatewayContract │ ← porta └───────────┬────────────┘ ┌───────────────┴────────────────┐ ┌──────────▼───────────┐ ┌────────────▼───────────┐ │ StripeGateway │ │ MercadoPagoGateway │ ← adaptadores │ "succeeded" → paid │ │ "approved" → paid │ (traduzem) └──────────┬───────────┘ └────────────┬───────────┘ ▼ ▼ API Stripe API Mercado Pago Depois vem a porta, o contrato que todo provedor precisa seguir:
interface PaymentGatewayContract { public function charge(Order $order): string; public function status(string $reference): PaymentStatus; public function refund(string $reference): void; } E um adaptador para cada provedor. É aqui, e só aqui, que o vocabulário de cada um aparece:
final class StripeGateway implements PaymentGatewayContract { public function __construct(private StripeApi $api) {} public function charge(Order $order): string { return $this->api->createPaymentIntent($order->amount_cents)->id; } public function status(string $reference): PaymentStatus { return match ($this->api->getPaymentIntent($reference)->status) { 'succeeded' => PaymentStatus::Paid, 'canceled' => PaymentStatus::Canceled, default => PaymentStatus::Pending, }; } public function refund(string $reference): void { $this->api->refundPaymentIntent($reference); } } final class MercadoPagoGateway implements PaymentGatewayContract { public function __construct(private MercadoPagoApi $api) {} public function charge(Order $order): string { return $this->api->createPixPayment($order->amount_cents)->id; } public function status(string $reference): PaymentStatus { return match ($this->api->getPayment($reference)->status) { 'approved' => PaymentStatus::Paid, 'cancelled', 'rejected' => PaymentStatus::Canceled, default => PaymentStatus::Pending, }; } public function refund(string $reference): void { $this->api->refundPayment($reference); } } Juntando tudo
A configuração diz qual provedor está ativo:
// config/payments.php return [ 'default' => env('PAYMENT_PROVIDER', 'stripe'), ]; Um resolver sabe qual adaptador corresponde a cada provedor:
final class PaymentGatewayResolver { public function for(PaymentProvider $provider): PaymentGatewayContract { return match ($provider) { PaymentProvider::Stripe => app(StripeGateway::class), PaymentProvider::MercadoPago => app(MercadoPagoGateway::class), }; } } E o container do Laravel junta os dois: entrega o adaptador do provedor ativo para qualquer classe que peça o contrato.
final class AppServiceProvider extends ServiceProvider { public function register(): void { $this->app->bind( PaymentGatewayContract::class, fn () => app(PaymentGatewayResolver::class) ->for(PaymentProvider::from(config('payments.default'))), ); } } Com isso, aquelas classes do começo do artigo deixam de saber qual provedor está por trás:
final class ReleaseProduct { public function __invoke(Order $order): void { if ($order->status !== PaymentStatus::Paid) { return; } $order->product->release(); } } final class RefundOrder { public function __construct(private PaymentGatewayContract $gateway) {} public function __invoke(Order $order): void { $this->gateway->refund($order->provider_reference); } } Se amanhã chegar a task de um terceiro provedor, o trabalho é criar um adaptador novo, adicionar um caso no enum PaymentProvider e uma linha no PaymentGatewayResolver. A tabela orders, o ReleaseProduct e o RefundOrder continuam iguais.




