Chain of Responsibility em Java: como transformamos um if de 600 linhas em um pipeline de autorização de pagamentos
Chain of Responsibility em Java: como transformamos um if de 600 linhas em um pipeline de autorização de pagamentos Padrão comportamental · Java 21 · Spring Boot 1. Base Fundamental Introdução O Chain of Responsibility (Corrente de Responsabilidade) é um padrão comportamental do catálogo GoF. Padrões comportamentais tratam de como objetos se comunicam e distribuem responsabilidades entre si - diferente dos criacionais (como objetos nascem) e dos estruturais (como objetos se compõem). A ideia central é simples: em vez de um único objeto decidir tudo, você monta uma fila de tratadores independentes. Cada um recebe a requisição, decide se consegue resolvê-la e, se não conseguir, passa adiante para o próximo. Quem chama não sabe - e não precisa saber - qual elo da corrente vai efetivamente responder. Se você já escreveu um Filter de Servlet, configurou o SecurityFilterChain do Spring Security ou usou middleware em Express/ASP.NET, você já usou Chain of Responsibility. É um dos padrões mais presentes em frameworks de produção e um dos menos aplicados conscientemente em código de aplicação. O Problema O padrão ataca uma dor específica e muito comum: um único método concentra uma sequência de decisões condicionais que cresce indefinidamente. O sintoma clássico é o método que começou com 20 linhas e hoje tem 600: public Resultado processar(Transacao t) { if (t.getValor() 80) { ... } else if (score > 50) { ... } else if (t.getPais().equals("DE") && t.getValor() > 30) { ... } else if ( /* mais 14 condições / ) { ... } } } Os problemas que isso gera não são estéticos: | Sintoma | Consequência prática | |---|---| | Toda regra nova altera a mesma classe | Violação do Open/Closed Principle; conflito de merge em todo sprint | | Regras acopladas dentro de um único método | Impossível testar a regra de blacklist sem instanciar todo o resto | | Ordem de execução implícita no aninhamento | Ninguém sabe dizer, olhando de fora, o que roda antes do quê | | Chamadas caras executadas cedo demais | Latência e custo pagos mesmo quando uma regra barata já teria negado | | Decisão anônima | O log diz "transação negada", mas não diz qual regra negou | O último ponto é o que costuma virar incidente às três da manhã. O penúltimo é o que vira fatura no fim do mês. Conceito A analogia mais próxima do dia a dia de software é o pipeline de filtros de um servidor web. Uma requisição HTTP entra e atravessa uma sequência: filtro de CORS, filtro de autenticação, filtro de autorização, filtro de rate limit. Cada filtro tem duas opções - interromper a requisição ali mesmo (401, 429) ou deixar passar para o próximo. Nenhum filtro conhece a existência do controller no fim da linha, e nenhum filtro precisa conhecer os outros filtros. Traduzindo para as peças do padrão: - Handler (Tratador): a interface ou classe abstrata que define o contrato trate(requisição) e guarda a referência para o próximo elo. - ConcreteHandler: cada tratador específico. Implementa uma regra e decide entre resolver ou delegar. - Client: quem monta a corrente e dispara a requisição na primeira ponta. Duas propriedades merecem destaque porque são o motivo de o padrão existir: - Desacoplamento entre emissor e receptor. O cliente chama corrente.autorizar(t) e recebe uma resposta. Não sabe quantos elos existem nem qual respondeu. - A ordem vira dado, não código. A sequência deixa de estar enterrada em aninhamento de if e passa a ser uma linha explícita e versionada em um arquivo de configuração. Reordenar a corrente é mudar uma linha - e, como veremos, reordenar é exatamente o que resolve o gargalo. 2. Desenvolvimento Cenário do Mundo Real O contexto é o microsserviço ms-pagamentos de uma plataforma de e-commerce que opera no Brasil e na Europa. Antes de enviar qualquer transação ao adquirente (PSP), o serviço precisa tomar uma decisão de autorização: APROVAR, NEGAR ou mandar para REVISÃO manual. Cinco regras compõem essa decisão, com custos de execução radicalmente diferentes: | # | Regra | O que faz | Custo | |---|---|---|---| | 1 | Dados cadastrais | Valor positivo, moeda suportada | ~0 ms (memória) | | 2 | Lista restritiva | Cliente bloqueado por chargeback anterior | ~2 ms (Redis) | | 3 | Velocidade transacional | Mais de 5 tentativas em 10 minutos | ~3 ms (Redis) | | 4 | Autenticação forte (SCA/3DS) | Emissor na UE e valor acima da isenção | ~0 ms (memória) | | 5 | Score antifraude | Consulta a bureau externo | ~400 ms, cobrado por consulta | No código legado, as cinco regras viviam em um único método de PagamentoService , e o score antifraude era consultado antes das validações baratas - herança de uma implementação em que o score era a única regra existente. O resultado: 100% das transações pagavam 400 ms e uma consulta faturada, inclusive aquelas com valor negativo ou de clientes já na lista restritiva, que seriam negadas de qualquer forma. O p95 do endpoint de checkout estava em 1,2 s, e a linha do bureau antifraude era a maior despesa variável do time. Nota de transparência: os números deste estudo de caso são ilustrativos, construídos para representar uma situação realista de engenharia. Se você for reproduzir o experimento, meça os seus. O objetivo da refatoração tinha dois lados: - Arquitetural: permitir adicionar, remover ou desligar regras sem tocar nas existentes. - De performance/custo: executar as regras da mais barata para a mais cara, com curto-circuito na primeira decisão. Chain of Responsibility resolve os dois com a mesma estrutura, e é por isso que ele foi o padrão escolhido em vez de Strategy - Strategy trocaria qual algoritmo roda, mas não resolveria a sequência nem o curto-circuito. Representação Visual Diagrama de Classes (UML) Estrutura do padrão aplicada exatamente ao código apresentado na próxima seção. classDiagram direction LR class AutorizacaoService { -RegraAutorizacao correnteDeAutorizacao -TransacaoRepository transacaoRepository +autorizar(Transacao) ResultadoAutorizacao } class RegraAutorizacao { > -RegraAutorizacao proxima +encadear(RegraAutorizacao) RegraAutorizacao +autorizar(Transacao) ResultadoAutorizacao #avaliar(Transacao) Optional~ResultadoAutorizacao~ } class RegraDadosCadastrais { #avaliar(Transacao) Optional~ResultadoAutorizacao~ } class RegraListaRestritiva { -ListaRestritivaRepository repositorio #avaliar(Transacao) Optional~ResultadoAutorizacao~ } class RegraVelocidadeTransacional { -ContadorTentativas contador #avaliar(Transacao) Optional~ResultadoAutorizacao~ } class RegraAutenticacaoForte { #avaliar(Transacao) Optional~ResultadoAutorizacao~ } class RegraScoreAntifraude { -ClienteAntifraude clienteAntifraude #avaliar(Transacao) Optional~ResultadoAutorizacao~ } class Transacao { > +String id +String clienteId +BigDecimal valor +String moeda +String paisEmissor } class ResultadoAutorizacao { > +Decisao decisao +String regra +String motivo } class Decisao { > APROVAR REVISAR NEGAR } class ListaRestritivaRepository { > +contemCliente(String) boolean } class ClienteAntifraude { > +consultarScore(Transacao) int } RegraAutorizacao o-- RegraAutorizacao : proxima RegraAutorizacao RegraAutorizacao : dispara RegraAutorizacao ..> Transacao : recebe RegraAutorizacao ..> ResultadoAutorizacao : produz ResultadoAutorizacao --> Decisao RegraListaRestritiva --> ListaRestritivaRepository RegraScoreAntifraude --> ClienteAntifraude O ponto mais importante do diagrama é a auto-associação RegraAutorizacao o-- RegraAutorizacao . É ela que caracteriza o padrão: o tratador conhece um tratador do mesmo tipo, o que permite montar uma corrente de tamanho arbitrário sem que nenhuma classe concreta conheça outra. Arquitetura de Software (alto nível) Onde a corrente se encaixa no ecossistema. flowchart TB subgraph canais["Canais"] WEB["Checkout Web"] APP["App Mobile"] end GW["API Gateway"] subgraph pagamentos["ms-pagamentos - Spring Boot"] CTRL["PagamentoController"] SVC["AutorizacaoService"] subgraph corrente["Corrente de Autorizacao"] direction TB R1["1 - Dados Cadastrais custo ~0ms"] R2["2 - Lista Restritiva custo ~2ms"] R3["3 - Velocidade custo ~3ms"] R4["4 - Autenticacao Forte custo ~0ms"] R5["5 - Score Antifraude custo ~400ms - pago"] R1 -->|"nao decidiu"| R2 R2 -->|"nao decidiu"| R3 R3 -->|"nao decidiu"| R4 R4 -->|"nao decidiu"| R5 end end REDIS[("Redis lista restritiva + contadores")] PG[("PostgreSQL transacoes e decisoes")] FRAUDE["API Antifraude bureau externo"] PSP["Adquirente / PSP"] KAFKA["Kafka topico decisoes-autorizacao"] NOTIF["ms-notificacoes"] BI["Data Lake / BI"] WEB --> GW APP --> GW GW --> CTRL CTRL --> SVC SVC --> R1 R2 -.-> REDIS R3 -.-> REDIS R5 -.-> FRAUDE SVC --> PG SVC -->|"decisao APROVAR"| PSP SVC --> KAFKA KAFKA --> NOTIF KAFKA --> BI Observe o efeito de custo: uma transação negada no elo 1 ou 2 nunca chega ao bureau externo. As setas pontilhadas representam I/O - e é justamente por serem I/O que essas regras ficam no fim da corrente. Implementação em Java Contratos de domínio public record Transacao( String id, String clienteId, BigDecimal valor, String moeda, String paisEmissor, Instant criadaEm ) {} public enum Decisao { APROVAR, REVISAR, NEGAR } public record ResultadoAutorizacao(Decisao decisao, String regra, String motivo) { public static ResultadoAutorizacao aprovar() { return new ResultadoAutorizacao( Decisao.APROVAR, "CORRENTE_COMPLETA", "Nenhuma regra bloqueou a transacao"); } public static ResultadoAutorizacao negar(String regra, String motivo) { return new ResultadoAutorizacao(Decisao.NEGAR, regra, motivo); } public static ResultadoAutorizacao revisar(String regra, String motivo) { return new ResultadoAutorizacao(Decisao.REVISAR, regra, motivo); } } O campo regra no resultado resolve o problema de observabilidade citado lá no começo: toda decisão carrega o nome de quem a tomou. O Handler abstrato - o coração do padrão public abstract class RegraAutorizacao { private RegraAutorizacao proxima; /* Encadeia e devolve o proximo elo, permitindo montagem fluente. */ public RegraAutorizacao encadear(RegraAutorizacao proxima
Comments
No comments yet. Start the discussion.