ADR 2 — Notificação acumulada em vez de exceção
A pergunta de projeto
Regra de negócio violada é o caso mais comum de “deu errado” num sistema corporativo: valor de devolução maior que o disponível, documento duplicado, canal sem fluxo configurado, voucher já usado. Não é falha do programa, é resultado previsto.
A pergunta é: como a regra reclama? A resposta se propaga por tudo — assinatura de método, tratamento no controller, formato da resposta HTTP, experiência de quem preenche o formulário. É barata de decidir uma vez e cara de mudar depois.
Por que exceção não serve
Exceção é o caminho idiomático do .NET e era o que a maior parte do código já fazia, então essa era a opção que não exigia justificativa nenhuma. Descartei por dois motivos, e o segundo demorou mais para eu enxergar.
O primeiro é comportamental: exceção interrompe no primeiro problema, e a tela quer mostrar os cinco campos errados de uma vez. Devolver um erro por vez transforma o preenchimento num jogo de tentativa e erro.
O segundo é conceitual, e é o que realmente decide. Exceção comunica “aconteceu algo que o programa não previu”, e regra de negócio violada é o contrário: o programa previu e tem opinião sobre isso. Usar exceção para as duas coisas apaga a distinção entre “o cliente errou o CPF” e “o banco caiu”, que merecem tratamento, log e alerta completamente diferentes. Quando essa distinção some do tipo, ela some também do monitoramento.
O Result foi o mais difícil de descartar
Um tipo que carrega sucesso ou erro, com o compilador forçando o tratamento, é tecnicamente superior no ponto exato onde a minha escolha é fraca: ele não deixa esquecer.
O que o inviabilizou não foi mérito, foi contexto. Result contamina toda assinatura no caminho, e
num código onde o idioma dominante é .NET convencional isso significa ou converter tudo, ou conviver
com duas convenções — que é pior que qualquer uma das duas isoladamente. Some a isso que Result
naturalmente carrega um erro; acumular vários exige envolvê-lo numa coleção e reintroduzir a
complexidade que ele existia para evitar.
A decisão
O domínio registra o que está errado e continua avaliando. Ao final, quem chamou pergunta se é válido e recebe a lista completa. Exceção fica reservada para o que é genuinamente excepcional: dependência fora, integração indisponível, estado impossível.
A fraqueza que eu aceitei
Nada obriga o chamador a verificar. Esquecer o if (!valido) produz sucesso silencioso, que é o
pior tipo de bug: não falha, só faz a coisa errada. Result não teria esse problema, e eu sabia
disso ao decidir.
Aceitei porque o custo é mitigável por disciplina e por ferramenta, enquanto o custo de duas convenções convivendo no mesmo código não é.
Como envelheceu
Melhor do que eu esperava, e a mitigação veio de onde eu não previa. Quando o mediator ganhou pipeline de comportamento (PMQ.Mediator), a verificação passou a acontecer num estágio antes do handler: o caso de uso nem executa se as notificações não estiverem limpas.
Isso resolveu estruturalmente a fraqueza que eu tinha aceitado como custo permanente. Não por planejamento — as duas decisões foram tomadas em momentos diferentes e simplesmente se encaixaram.
A confirmação externa veio depois: PMQ.Notifications é o mais baixado dos cinco pacotes que publiquei, com mais downloads que qualquer um dos outros. É o problema mais universal da suíte.
O que eu faria diferente
Faria o caso não verificado falhar alto em desenvolvimento: um objeto de notificação que, ao ser descartado sem consulta, reclama em tempo de execução fora de produção. Recuperaria boa parte da garantia que eu abri mão, sem o custo de propagação de tipo. É pequeno, é óbvio em retrospecto, e eu não fiz.
Ver também
PMQ.Notifications · Suíte PMQ · Modelagem de domínio, DDD e CQRS