Contei os if de um serviço de preço: 1.043 ramificações e só 34 mudaram no último ano
A pergunta que chegou até mim era curta: "vale comprar um motor de regras?". Recebi acesso de leitura a um serviço de precificação em Node com TypeScript, uns 40 mil linhas, seis arquivos dentro de src/pricing, três pessoas no histórico do repositório e nenhuma delas ainda na empresa. Dava para responder por opinião em dois minutos. Preferi medir três coisas antes, e as três couberam numa tarde.
As medições foram: quantas ramificações de condição existem no cálculo, quantas delas mudaram nos últimos doze meses, e quantos números do código divergem da planilha que o financeiro usa para fechar o mês.
1. Quantas ramificações existem
A contagem é grosseira de propósito. Serve para ordem de grandeza, não para relatório:
$ rg --no-filename -o -e '\bif \(' -e '\belse if\b' -e '\bcase .*:' -e '\?\?' src/pricing | wc -l
1043
$ rg -c 'if \(' src/pricing/*.ts
src/pricing/commission.ts:412
src/pricing/discount.ts:298
src/pricing/tax.ts:171
src/pricing/credit-limit.ts:96
src/pricing/freight.ts:44
src/pricing/index.ts:22
Mil e quarenta e três pontos de decisão em seis arquivos. É o tipo de número que faz qualquer um pedir orçamento de Drools na mesma semana. Só que ele não responde nada sozinho. Regra parada custa leitura, e leitura é barata. O que custa caro é regra que muda.
2. Quantas mudaram em doze meses
Aqui a conta muda de figura:
$ git log --since='12 months ago' -p --unified=0 -- src/pricing \
| rg '^\+' \
| rg -e '\bif \(' -e '\belse if\b' -e '\bcase .*:' \
| sort -u | wc -l
34
$ git log --since='12 months ago' --format='%ad' --date=format:'%Y-%m' -- src/pricing \
| sort | uniq -c
1 2025-09
4 2025-11
2 2026-01
11 2026-03
3 2026-05
9 2026-07
2 2026-08
Trinta e quatro linhas de condição tocadas no ano inteiro, dentro de 1.043. Pouco mais de 3%. E as mudanças se concentram em dois meses, março e julho, que na empresa são os meses de revisão de tabela comercial.
Dei uma olhada no que são essas 34. Vinte e sete trocam um número: percentual de comissão, teto de desconto, faixa de quantidade, alíquota. Sete mexem na estrutura da condição, e seis dessas sete estão no mesmo arquivo, commission.ts.
Esse é o gráfico que decide, e ele não aparece em nenhuma apresentação de fornecedor de motor de regras. Vinte e sete pedidos de troca de número passaram por tarefa, fila, revisão e deploy durante um ano. Cada um deles esperou dias por uma alteração que, no fim, é um UPDATE.
3. Quantos números do código discordam da planilha
Última medição, e a mais chata de fazer. Extraí as constantes numéricas dos arquivos e coloquei ao lado da planilha que o financeiro usa no fechamento:
$ rg -o -e '0\.[0-9]{2,4}' -e '=== ?[0-9]{2,4}' src/pricing/*.ts \
| sort | uniq -c | sort -rn | head -n 5
18 src/pricing/commission.ts:0.12
9 src/pricing/discount.ts:0.08
7 src/pricing/commission.ts:0.05
6 src/pricing/tax.ts:0.065
4 src/pricing/credit-limit.ts:0.15
Saíram 61 constantes. Comparei uma a uma com as células da planilha, na mão, em algo perto de 40 minutos. Nove divergiam. A pior era um 0.12 no código contra 0.125 na aba de comissão do canal de parceiros, valendo desde fevereiro pelo histórico de versões do arquivo. O boleto sai do sistema, a comissão é paga pela planilha, e a diferença de meio ponto rodou uns cinco meses sem ninguém acender uma luz.
Não existe teste que pegue isso. O código está certo em relação a si mesmo. A verdade mora em dois lugares e ninguém escreveu qual dos dois manda.
Parâmetro cabe em tabela, encadeamento não
Com as três medições na mão, o corte fica óbvio, e ele não é entre "código" e "motor de regras". É entre número e encadeamento.
Número cabe em tabela, com dono e data de vigência:
select value
from pricing_rule
where key = 'commission.partner.default'
and valid_from <= now()
order by valid_from desc
limit 1
Encadeamento não cabe. Este trecho é o formato real do que estava no arquivo de desconto, com os nomes trocados:
function discountFor(order: Order): number {
if (order.qty >= 50) {
if (order.customer.type === 'distributor') {
if (order.contract.signedAt < new Date('2023-01-01')) {
return 0.065
}
return 0.05
}
return 0.08
}
return 0
}
Tente representar isso em colunas de uma tabela. Você vai acabar escrevendo um interpretador dentro do seu próprio sistema, e aí construiu um motor de regras sem depurador, sem teste e sem ninguém que o conheça além de você.
O que eu faria nesse repositório: os quatro valores viram linha em pricing_rule com vigência, a forma da condição continua onde está, e ganha um teste com nome em português descrevendo o caso comercial. Cinco dos seis arquivos ficam parados como estão hoje, porque medir mostrou que eles não incomodam ninguém.
A pergunta "How can one manage thousands of IF...THEN...ELSE rules?" está no Software Engineering Stack Exchange com 109.620 visualizações e 18 respostas. Nenhuma delas é curta. Depois de rodar esses três comandos eu entendo por quê: a resposta depende de um número que só existe no seu repositório, e quase ninguém mediu o próprio.
Duas perguntas para quem já passou por isso. Alguém aqui roda motor de regras em produção com o pessoal de negócio operando de verdade, sem chamado para a TI? E quando vocês tiraram parâmetro do código para uma tabela, como resolveram vigência retroativa, aquele caso de recalcular um pedido de três meses atrás com a regra da época?
Originalmente publicado no blog da Revin: https://revin.com.br/pt/blog/mil-regras-negocio-codigo-motor-regras-planilha