DEV Community

Strategy Pattern na Prática: Como Desacoplar Regras de Pagamento sem Quebrar seu Sistema

Strategy Pattern na Prática: Como Desacoplar Regras de Pagamento sem Quebrar seu Sistema


1. Base Fundamental (O Problema e a Teoria)

Introdução

O Strategy é um Design Pattern comportamental, catalogado originalmente pela Gang of Four (GoF). Padrões comportamentais tratam da comunicação e da distribuição de responsabilidades entre objetos — e o Strategy resolve especificamente um problema muito comum: como permitir que um algoritmo varie independentemente de quem o utiliza.

Em vez de embutir uma regra de negócio dentro de uma classe com if/else ou switch, o Strategy propõe encapsular cada variação do algoritmo em sua própria classe, todas implementando uma interface comum. Quem usa o algoritmo (o "contexto") não precisa saber qual variação está em execução — apenas que ela cumpre um contrato.

O Problema

Imagine um sistema de e-commerce que precisa processar pagamentos via cartão de crédito, PIX e boleto. A primeira versão, "rápida e suja", normalmente nasce assim:

public void processarPagamento(String tipo, double valor) {
    if (tipo.equals("CARTAO")) {
        // lógica de cartão...
    } else if (tipo.equals("PIX")) {
        // lógica de PIX...
    } else if (tipo.equals("BOLETO")) {
        // lógica de boleto...
    }
}
Enter fullscreen mode Exit fullscreen mode

Esse código funciona — até a empresa decidir aceitar um quarto método de pagamento, ou até uma nova regulamentação mudar a forma de validar o PIX. Cada mudança obriga a alterar uma classe que já está em produção, violando o Princípio Aberto/Fechado (o "O" do SOLID): o sistema deveria estar aberto para extensão, mas fechado para modificação. Além disso, esse método tende a crescer indefinidamente, misturando responsabilidades e dificultando testes unitários isolados por tipo de pagamento.

Conceito

O Strategy resolve isso definindo:

  1. Uma interface comum (a "estratégia") que declara o método do algoritmo.
  2. Uma classe concreta para cada variação do algoritmo, implementando essa interface.
  3. Um contexto, que recebe uma estratégia (normalmente por injeção de dependência) e delega a execução a ela, sem conhecer os detalhes internos.

Uma analogia simples: pense em um carregador de celular universal com pontas intercambiáveis (USB-C, Lightning, Micro-USB). O carregador (contexto) sempre entrega energia da mesma forma para quem o usa; a ponta (estratégia) é que muda conforme o aparelho. Trocar de aparelho não exige comprar um carregador novo — só trocar a ponta. No software, "trocar a ponta" significa registrar uma nova classe de estratégia, sem tocar no restante do sistema.


2. Desenvolvimento (Estudo de Caso, Arquitetura e Código)

Cenário do Mundo Real

Vamos usar um cenário realista de engenharia: um serviço de checkout de e-commerce que precisa processar pagamentos por Cartão de Crédito, PIX e Boleto Bancário, cada um com regras, taxas e integrações externas diferentes (gateway de cartão, API do Banco Central/PSP para PIX, e um serviço de emissão de boletos). O time de produto avisa que, em breve, outros métodos (como carteiras digitais) também precisarão ser suportados — e que auditoria exige testar cada regra de pagamento isoladamente.

O Strategy encaixa perfeitamente aqui: o PaymentService (contexto) não deve saber como cada pagamento é processado, apenas que existe uma estratégia capaz de processá-lo.

Representação Visual

Diagrama UML de Classes — estrutura do padrão aplicada ao cenário:

classDiagram
    class PaymentStrategy {
        <<interface>>
        +pay(amount: double) PaymentResult
    }

    class CreditCardPaymentStrategy {
        +pay(amount: double) PaymentResult
    }

    class PixPaymentStrategy {
        +pay(amount: double) PaymentResult
    }

    class BoletoPaymentStrategy {
        +pay(amount: double) PaymentResult
    }

    class PaymentContext {
        -strategy: PaymentStrategy
        +setStrategy(strategy: PaymentStrategy)
        +executePayment(amount: double) PaymentResult
    }

    class PaymentResult {
        +success: boolean
        +transactionId: String
        +message: String
    }

    PaymentStrategy <|.. CreditCardPaymentStrategy
    PaymentStrategy <|.. PixPaymentStrategy
    PaymentStrategy <|.. BoletoPaymentStrategy
    PaymentContext o--> PaymentStrategy
    PaymentContext ..> PaymentResult

Desenho de Arquitetura de Software — onde o padrão se encaixa no ecossistema do checkout:

flowchart LR
    Client["App / Frontend do Checkout"] --> API["Checkout API (REST)"]
    API --> Context["PaymentContext (Strategy Selector)"]

    Context --> CC["CreditCardPaymentStrategy"]
    Context --> PIX["PixPaymentStrategy"]
    Context --> Boleto["BoletoPaymentStrategy"]

    CC --> Gateway["Gateway de Cartão (ex: Adquirente)"]
    PIX --> PSP["PSP / API PIX do Banco Central"]
    Boleto --> BankAPI["API de Emissão de Boletos"]

    Context --> DB[("Banco de Dados de Pedidos")]
    Context --> Events["Fila de Eventos (ex: Kafka/SQS) -> Notificação ao Microsserviço de Pedidos"]

💡 Dica de entrega: o código Mermaid acima pode ser colado em mermaid.live para gerar a imagem em alta resolução e exportar como PNG/SVG — necessário porque Medium e LinkedIn não renderizam Mermaid nativamente (o Dev.to renderiza).

Implementação em Java

// Interface da estratégia
public interface PaymentStrategy {
    PaymentResult pay(double amount);
}
Enter fullscreen mode Exit fullscreen mode
// Objeto de retorno, desacoplado da estratégia concreta
public class PaymentResult {
    private final boolean success;
    private final String transactionId;
    private final String message;

    public PaymentResult(boolean success, String transactionId, String message) {
        this.success = success;
        this.transactionId = transactionId;
        this.message = message;
    }

    public boolean isSuccess() { return success; }
    public String getTransactionId() { return transactionId; }
    public String getMessage() { return message; }
}
Enter fullscreen mode Exit fullscreen mode
// Estratégia concreta: Cartão de Crédito
public class CreditCardPaymentStrategy implements PaymentStrategy {

    private final CreditCardGatewayClient gatewayClient;

    public CreditCardPaymentStrategy(CreditCardGatewayClient gatewayClient) {
        this.gatewayClient = gatewayClient;
    }

    @Override
    public PaymentResult pay(double amount) {
        var response = gatewayClient.charge(amount);
        return new PaymentResult(
            response.isApproved(),
            response.getAuthorizationCode(),
            response.isApproved() ? "Pagamento aprovado no cartão" : "Pagamento recusado"
        );
    }
}
Enter fullscreen mode Exit fullscreen mode
// Estratégia concreta: PIX
public class PixPaymentStrategy implements PaymentStrategy {

    private final PixApiClient pixApiClient;

    public PixPaymentStrategy(PixApiClient pixApiClient) {
        this.pixApiClient = pixApiClient;
    }

    @Override
    public PaymentResult pay(double amount) {
        var qrCode = pixApiClient.gerarCobranca(amount);
        return new PaymentResult(true, qrCode.getTxId(), "Cobrança PIX gerada com sucesso");
    }
}
Enter fullscreen mode Exit fullscreen mode
// Estratégia concreta: Boleto
public class BoletoPaymentStrategy implements PaymentStrategy {

    private final BoletoService boletoService;

    public BoletoPaymentStrategy(BoletoService boletoService) {
        this.boletoService = boletoService;
    }

    @Override
    public PaymentResult pay(double amount) {
        var boleto = boletoService.emitir(amount);
        return new PaymentResult(true, boleto.getNossoNumero(), "Boleto emitido, aguardando compensação");
    }
}
Enter fullscreen mode Exit fullscreen mode
// Contexto: não conhece os detalhes de cada estratégia
public class PaymentContext {

    private PaymentStrategy strategy;

    public void setStrategy(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

    public PaymentResult executePayment(double amount) {
        if (strategy == null) {
            throw new IllegalStateException("Nenhuma estratégia de pagamento definida");
        }
        return strategy.pay(amount);
    }
}
Enter fullscreen mode Exit fullscreen mode
// Uso no serviço de checkout
public class CheckoutService {

    private final PaymentContext paymentContext = new PaymentContext();

    public PaymentResult finalizarCompra(String metodoPagamento, double valor) {
        PaymentStrategy strategy = switch (metodoPagamento) {
            case "CARTAO" -> new CreditCardPaymentStrategy(new CreditCardGatewayClient());
            case "PIX" -> new PixPaymentStrategy(new PixApiClient());
            case "BOLETO" -> new BoletoPaymentStrategy(new BoletoService());
            default -> throw new IllegalArgumentException("Método de pagamento não suportado: " + metodoPagamento);
        };

        paymentContext.setStrategy(strategy);
        return paymentContext.executePayment(valor);
    }
}
Enter fullscreen mode Exit fullscreen mode

Observação: o switch que resta aqui é apenas de seleção/instanciação da estratégia — normalmente resolvido por injeção de dependência ou uma Factory/Map<String, PaymentStrategy> em aplicações Spring, o que elimina até esse ponto de decisão. O importante é que a lógica de cada pagamento está isolada e testável de forma independente.

Prós e Contras

Vantagens:

  • Elimina condicionais extensas (if/else/switch) espalhadas pelo código de negócio.
  • Facilita a adição de novos métodos de pagamento sem alterar classes existentes (Aberto/Fechado).
  • Cada estratégia pode ser testada isoladamente, com mocks das integrações externas.
  • Reduz o acoplamento entre a regra de negócio e os detalhes de integração de cada gateway.

Desvantagens / trade-offs:

  • Aumenta o número de classes no projeto, o que pode parecer "excesso de engenharia" para poucos algoritmos.
  • O cliente do contexto precisa saber qual estratégia escolher, o que exige algum mecanismo de seleção (fábrica, mapa, configuração).
  • Se as estratégias forem muito simples, o padrão pode adicionar complexidade desnecessária.

3. Conclusão

Resumo

O Strategy resolve um problema recorrente em sistemas de pagamento (e em muitos outros domínios): a proliferação de condicionais acopladas a regras de negócio voláteis. Ao isolar cada algoritmo em sua própria classe, o padrão torna o sistema mais fácil de estender, testar e manter, alinhado ao Princípio Aberto/Fechado.

Visão Pessoal

Implementar o Strategy neste cenário deixou claro como um padrão comportamental "simples" no papel resolve um problema real de manutenção. A parte mais interessante foi perceber que o ganho não é só técnico — testar cada meio de pagamento isoladamente, sem precisar simular todos os outros, também acelera o desenvolvimento e reduz o medo de "quebrar algo" ao adicionar um novo método.

E você?

Já precisou refatorar um bloco de if/else que crescia a cada nova regra de negócio? Qual solução você adotou? Deixe nos comentários! 🚀

Top comments (0)