Strategy Pattern em Java: Isolando Regras de Cálculo Fiscal em um Sistema B2B
DEV Community

Strategy Pattern em Java: Isolando Regras de Cálculo Fiscal em um Sistema B2B

Como separar regras de negócio que mudam em ritmos diferentes - e por que o switch é o lugar errado para elas. Introdução O Strategy é um Padrão de Projeto Comportamental, catalogado originalmente pela Gang of Four. Padrões comportamentais tratam de como objetos se comunicam e distribuem responsabilidades entre si - diferente dos criacionais, que tratam de como objetos são instanciados, e dos estruturais, que tratam de como objetos se compõem. A definição formal é curta: o Strategy encapsula uma família de algoritmos intercambiáveis, cada um em sua própria classe, e permite que o algoritmo varie independentemente do cliente que o utiliza. A definição prática é ainda mais curta: é o padrão que transforma a pergunta "qual regra eu aplico agora?" de uma estrutura condicional em uma escolha de objeto. O problema: o switch que ninguém quer tocar Todo sistema que cresce acumula um método específico. Ele começa com dois casos, ganha um terceiro em uma sprint apertada e, alguns anos depois, tem centenas de linhas e um comentário do tipo // não mexer sem falar com o time de faturamento . O sintoma visível é feio, mas o sintoma não é o problema. O problema real é acoplamento temporal: regras que mudam em cadências independentes foram colocadas fisicamente na mesma unidade de código. Quando a regra A muda, você é obrigado a abrir, recompilar e reimplantar o mesmo arquivo que executa a regra B - mesmo que B não tenha mudado nada. Isso transforma cada alteração pequena em risco grande. E risco grande produz um comportamento defensivo previsível: ninguém quer mexer no método, então as pessoas contornam com condicionais extras em vez de corrigir a estrutura, e o método piora a cada iteração. O Strategy ataca exatamente esse acoplamento. Ele separa o que varia do que permanece estável, colocando cada variação em uma classe própria. Conceito: a analogia da fonte de energia Pense em um notebook e nas tomadas do mundo. O notebook não tem, internamente, uma estrutura condicional perguntando "estou no Brasil? estou no Reino Unido? estou no Japão?" para decidir como converter a energia. Se tivesse, cada país novo exigiria um firmware novo no notebook. O que existe é um contrato: a fonte entrega uma tensão estável ao notebook. Como essa tensão foi obtida - de 127V, 220V ou 240V - é problema de quem implementa o contrato, não de quem o consome. O Strategy é isso em código: - O contrato é uma interface. - Cada variação do algoritmo é uma classe que implementa essa interface. - O contexto (o notebook) conhece apenas a interface, nunca as implementações concretas. A consequência importante: a lógica que decide como calcular fica isolada da lógica que decide quando calcular. Estudo de caso: motor de cálculo fiscal em um sistema B2B Para demonstrar o padrão em um contexto mais próximo dos desafios encontrados em sistemas corporativos, vamos utilizar como exemplo um motor de cálculo fiscal em uma plataforma B2B. ⚠️ Aviso: Os valores, alíquotas e fórmulas apresentados neste artigo são simplificados e possuem finalidade exclusivamente didática. Eles não devem ser utilizados para cálculo tributário real ou tomada de decisão fiscal. O sistema legado A plataforma emite notas para clientes de perfis diferentes, e o valor dos tributos depende do regime tributário do cliente: Simples Nacional, Lucro Presumido ou Lucro Real. O código original concentrava tudo em um único método: public BigDecimal calcularImpostos(Pedido pedido, Cliente cliente) { BigDecimal total = BigDecimal.ZERO; if (cliente.getRegime().equals("SIMPLES")) { // faixas de receita, parcela a deduzir, alíquota efetiva } else if (cliente.getRegime().equals("PRESUMIDO")) { // alíquotas fixas sobre a receita } else if (cliente.getRegime().equals("REAL")) { // alíquotas maiores, com desconto de créditos apropriados } return total; } Por que isso é um problema de engenharia, e não só código feio Três características desse domínio tornam o acoplamento caro: 1. As regras mudam em datas diferentes e por motivos diferentes. Cada regime segue sua própria trilha legislativa. Uma alteração em um não tem relação nenhuma com o outro, mas ambos vivem no mesmo arquivo. 2. Os algoritmos consomem entradas diferentes. O Simples precisa da receita bruta acumulada do cliente para descobrir em qual faixa ele está. O Lucro Real precisa dos créditos apropriados no período. O Lucro Presumido não usa nenhum dos dois. Não existe "extrair um método comum" aqui, porque os inputs divergem. 3. O raio de explosão de um erro é o faturamento inteiro. Uma regressão introduzida ao ajustar a faixa do Simples pode derrubar a emissão de notas dos clientes de Lucro Real, porque compartilham o mesmo caminho de execução. Esse é o formato clássico de problema que o Strategy resolve: variação de algoritmo, com cadência de mudança independente e alto custo de erro. A partir daqui, o foco é o padrão; a parte fiscal existe apenas para dar peso realista ao cenário. Escopo do exemplo Para manter o artigo focado no padrão, as alíquotas e faixas são constantes dentro de cada estratégia. Em um sistema real elas viriam de configuração versionada com data de vigência - uma decisão de infraestrutura ortogonal ao Strategy. Todo o código apresentado abaixo é Java puro e compila sem framework. Representação visual Figura 1 - Diagrama UML do Strategy Pattern aplicado ao motor fiscal. O diagrama está restrito às classes que compõem o padrão. Pedido , Cliente e as exceções aparecem no código como tipos de apoio, mas incluí-los aqui tiraria o foco da estrutura que importa. Repare no losango vazio, indicando agregação, entre MotorFiscal e CalculadoraImposto : o motor usa as estratégias, mas não as cria. Esse detalhe é o que separa um Strategy real de um switch disfarçado. Figura 2 - Arquitetura de alto nível do sistema de cálculo fiscal. Dois pontos que este desenho comunica e o UML não: A consulta ao banco acontece antes do motor. É o dado do cliente que determina qual estratégia será executada - a seleção não é uma decisão tomada em código, é uma consequência de dados que chegam em tempo de execução. O Módulo Fiscal é uma fronteira interna, não um serviço remoto. O Strategy aqui é uma decisão de organização interna. Ele não introduz nenhuma chamada de rede, e é importante que isso fique visível: padrões de projeto e padrões de arquitetura distribuída resolvem problemas diferentes. Implementação em Java Tipos de apoio public enum RegimeTributario { SIMPLES_NACIONAL, LUCRO_PRESUMIDO, LUCRO_REAL } public record Cliente(RegimeTributario regimeTributario, BigDecimal receitaBruta12Meses, String uf) {} public record Pedido(BigDecimal valorTotal, BigDecimal creditosApropriados) {} public class RegimeNaoSuportadoException extends RuntimeException { public RegimeNaoSuportadoException(RegimeTributario regime) { super("Nenhuma calculadora registrada para o regime: " + regime); } } public class LimiteSimplesExcedidoException extends RuntimeException { public LimiteSimplesExcedidoException(BigDecimal receita) { super("Receita acumulada fora das faixas configuradas: " + receita); } } O contrato public record ContextoFiscal( BigDecimal valorBruto, BigDecimal receitaBruta12Meses, BigDecimal creditosApropriados, String uf ) { public static ContextoFiscal de(Pedido pedido, Cliente cliente) { return new ContextoFiscal( pedido.valorTotal(), cliente.receitaBruta12Meses(), pedido.creditosApropriados(), cliente.uf() ); } } public record ResultadoImposto( BigDecimal total, RegimeTributario regimeAplicado, Map detalhamento ) {} public interface CalculadoraImposto { ResultadoImposto calcular(ContextoFiscal contexto); RegimeTributario getRegime(); } O método getRegime() merece atenção. Ele existe para que cada estratégia se identifique, em vez de alguém externo precisar mapeá-la. É esse detalhe que permite eliminar a estrutura condicional de verdade, em vez de apenas empurrá-la para uma factory. As estratégias public class SimplesNacionalCalculadora implements CalculadoraImposto { private record Faixa(BigDecimal limite, BigDecimal aliquotaNominal, BigDecimal parcelaDeduzir) {} // Tabela reduzida e simplificada para fins didáticos. private static final List FAIXAS = List.of( new Faixa(bd("180000"), bd("0.0400"), bd("0")), new Faixa(bd("360000"), bd("0.0730"), bd("5940")), new Faixa(bd("720000"), bd("0.0950"), bd("13860")), new Faixa(bd("1800000"), bd("0.1070"), bd("22500")) ); @Override public ResultadoImposto calcular(ContextoFiscal contexto) { BigDecimal receita = contexto.receitaBruta12Meses(); Faixa faixa = FAIXAS.stream() .filter(f -> receita.compareTo(f.limite()) new LimiteSimplesExcedidoException(receita)); // Alíquota efetiva = (receita x alíquota nominal - parcela a deduzir) / receita BigDecimal aliquotaEfetiva = receita.multiply(faixa.aliquotaNominal()) .subtract(faixa.parcelaDeduzir()) .divide(receita, 6, RoundingMode.HALF_UP); BigDecimal imposto = contexto.valorBruto() .multiply(aliquotaEfetiva) .setScale(2, RoundingMode.HALF_UP); return new ResultadoImposto( imposto, RegimeTributario.SIMPLES_NACIONAL, Map.of("IMPOSTO_UNIFICADO", imposto, "ALIQUOTA_EFETIVA", aliquotaEfetiva) ); } @Override public RegimeTributario getRegime() { return RegimeTributario.SIMPLES_NACIONAL; } private static BigDecimal bd(String valor) { return new BigDecimal(valor); } } public class LucroPresumidoCalculadora implements CalculadoraImposto { // Alíquotas ilustrativas. private static final BigDecimal ALIQUOTA_A = new BigDecimal("0.0065"); private static final BigDecimal ALIQUOTA_B = new BigDecimal("0.0300"); @Override public ResultadoImposto calcular(ContextoFiscal contexto) { BigDecimal base = contexto.valorBruto(); BigDecimal tributoA = base.multiply(ALIQUOTA_A).setScale(2, RoundingMode.HALF_UP); BigDecimal tributoB = base.multiply(ALIQUOTA_B).setScale(2, RoundingMode.HALF_UP); return new ResultadoImposto( tributoA.add(tributoB), RegimeTributario.LUCRO_PRESUMIDO, Map.of("TRIBUTO_A", tributoA, "TRIBUTO_B", tributoB) ); } @Override public RegimeTributario getRegime() { return RegimeTr

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.