6

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

Carregando publicação patrocinada...
2

Meus 2 cents,

Parabens pelo post !

Ele mostra um pouco da realidade: a ideia eh linda na teoria ai vai o DEV e acaba colocando como hard code, e quem vier depois que se vire.

Sobre a ultima pergunta, "...como resolveram vigência retroativa..." aqui vai a resposta que um DEV (meu chefe na epoca) resolveu em uma folha de pagamento em C ainda nos anos 80/90: as regras eram escritas em pseudo-codigo e versionadas por data (inclusive variaveis), entao quando ia gerar um determinado calculo (seja para todos os funcionarios, seja para 1 individual) o proprio sistema pegava a REF da folha que ia ser calculada e buscava as regras ativas na epoca. Isso era a parte facil, a complicada era determinar alguns calculos em cascata que influenciavam outros meses (principalmente contabilidade).

No final dos anos 90, em uma fabrica de software que trabalhei, a solucao vinha atraves de TRIGGERs e STORED PROCEDURES para armazenar as regras de negocio e como a empresa tinha ISO 9000, qualquer alteracao tinha de ser documentada pelos analistas/DEVs e passava pelo time de DBA que implementava: nao era o ideal (dava gargalo) mas pelo menos mantinha o processo sob controle (e sim, quando tinha de rodar processos com regras antigas era um caos).

Hoje resolvo de forma semelhante a folha de pagamento do exemplo acima: dentro do possivel regras versionadas e estanques chamadas e abstraidas do fluxo principal.

Saude e Sucesso !


Este post foi favoritado via extensão TABNEWS FAVORITOS

Tem curiosidade sobre IA ? Da uma olhada no meu LIVRO: IA PARA ENGENHEIROS

2

Encadeamento não cabe. Este trecho é o formato real do que estava no arquivo de desconto, com os nomes trocados:
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ê.

Eu achei esse modelo simples:

qtde_minimatipodt_inicialdt_finalvalor
50distributor1920-01-012023-01-010.065
50distributor2023-01-020.050
50other1920-01-010.080

Alí ele usa pela data do contrato, se fosse necessário, poderia usar colunas de validade.

Para cada critério que tiver para saber o preço ou desconto, basta adicionar uma coluna na tabela. Na consulta, filtra por ele.

1

Quando tive que fazer algo similar, criamos uma tabela de regras com uma coluna indicando se está ativa ou não. Ao cadastrar algo que dependa da regra, ela aponta para o ID da regra. Quando ela muda, o sistema cria um novo registro e desativa o anterior. Assim, eu tenho todo o histórico de regras, permitindo-me reconstruir qualquer período histórico, mas a tela de associação só permite usar as regras ativas, com a versão mais recente dela.

Nós tínhamos ainda colunas de dt_criacao, dt_desativacao, usuario_criacao, usuario_desativacao. Assim temos rastreio de quem criou e desativou cada uma das regras, bem como data e hora em que isso ocorreu.

1

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?

Eu considero que desenvolvedor tem uma certa autonomia
Limitada, é claro