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...
}
}
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:
- Uma interface comum (a "estratégia") que declara o método do algoritmo.
- Uma classe concreta para cada variação do algoritmo, implementando essa interface.
- 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);
}
// 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; }
}
// 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"
);
}
}
// 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");
}
}
// 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");
}
}
// 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);
}
}
// 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);
}
}
Observação: o
switchque resta aqui é apenas de seleção/instanciação da estratégia — normalmente resolvido por injeção de dependência ou umaFactory/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)