Você já deve ter ouvido algum requisito assim: Caso seja maior que 80%, deve executar A(), caso contrário B(), em ambos os casos precisa executar C(). Do meu lado estou aqui torcendo para você não ter seguido essa regra ao pé da letra. É sobre isso que falaremos hoje.
Esse assunto é dos mais básicos sobre modelagem, não acho que devamos ter esse papo, acho que já passamos dessa fase. Mas para mim é importante ter algo mais sólido do que uma conversa para encaminhar no futuro. Esse é um post documental e não me admira se achar o assunto chato ou mesmo básico: faz parte!
O problema
Fato é que quando alguém com o viés de negócio diz para alguém que é técnico algo como:
- Se o valor for maior XX faça isso.
- Se o status for tal então execute aquilo.
- Se a empresa (item de cadastro) for X, faça A, se a empresa for Y, faça B.
Esses comportamentos NÃO PODEM ser expressos no código exatamente como descritos acima.
A naturalidade com que regras assim são expressas por profissionais que não possuem skill de desenvolvimento, pode potencialmente induzir ao erro primário de escrever a regra assim como lhe foi passada, principalmente se você for afoito. Modelar requisitos assim com IF’s mágicos, baseados em números e ids mágicos, lhe tratá problemas. Na minha opinião, já é um problema. A mudança começa pela dificuldade de compreender o que determinada regra expressa, qual seu impacto, aumenta a dificuldade de testar, gerando comportamentos que só podem ser expressos quando um determinado ID chega ao número X, ou ainda tornando difícil o troubleshooting de algum comportamento inesperado e não testado previamente. Essa é uma abordagem nociva.
Casos assim podem ser resolvidos de diversas formas, eu aconselho discriminadores de comportamento. Acredito que possam ter outro nome, e não esse neologismo que criei, mas na prática são elementos que adicionados à modelagem permitem discriminar comportamentos alternativos ou adicionais. Pode ser uma propriedade que indica um comportamento. Então em vez de programar algo como um IF em função de um id, para decidir se uma fórmula de cálculo é X ou Y, você expressa em uma nova propriedade qual fórmula de cálculo usar. Um desdobramento possível pode ser a utilização de uma factory para criar uma instância de uma calculadora específica ou um mero if em seu código, dependendo da complexidade do que estamos falando. Esse metadado, dado que descreve o dado, ajuda a aumentar a flexibilidade, testabilidade, resiliência e reduz a possibilidade de bugs.
Exemplo: Status
Vamos a um exemplo real. Algo que ocorreu há alguns dias em um dos projetos em que participo.
Estou tocando um projeto muito simples, uma LOB App, simples, cadastral, de tamanho mediano. Não tem grandes requisitos, não tem grandes desafios técnicos, mas é extensa. Algo que cuida de um processo que parece com o Jira, ou qualquer ferramenta de controle de chamados. Sem meta-programação e extensibilidade de jira. Portanto é realmente algo simples. Entre diversas outras coisas, tenho um status de atividade para controlar, assim:
- Tenho uma classe que representa um Status
- Naturalmente essa classe Status está refletida no banco com uma uma tabela Status, espelhada com os mesmos campos/atributos, tudo mapeado com um ORM.
- Os status possíveis são “Aberto”, “Em andamento”, “Resolvido” e “Cancelado” (existem outros, mas não são relevantes para o exemplo).
- Haviam 2 regras a serem cuidadas:
- O status inicial é o status de Aberto.
- Precisamos montar view (tela/página/etc) para exibir registros com o status Resolvido e Cancelado.
- Precisamos montar view (tela/página/etc) para exibir registros com o status Em Andamento.
- Precisamos montar view (tela/página/etc) para exibir registros com o status Abertos.
A questão que reside nesse problema é simples, mas a solução não pode levar em conta ao pé da letra o que foi solicitado. Eu não estou dizendo para não realizar o que foi pedido, mas proponho pequenas reflexões a ponto de propor uma mudanças na modelagem que enderecem esses pontos, afim de melhorar seu código, sua testabilidade, e tornar a solução mais flexível e resiliente às mudanças. Dependendo da estratégia adotada, a adição de um simples novo resultado poderia demandar algo muito mais complexo do que fazer um mísero insert no banco.
Solução
Ignorando os problemas que me levaram a escrever esse post, vamos para uma solução que eu geralmente adoto e considero uma das alternativas adequadas. A propósito, não considero essa a única forma correta, mas uma entre diversas soluções elegantes possíveis.
A solução que escolhi para esse problema foi adicionar 2 atributos à classe, consequentemente 2 colunas à tabela: IsInitial e IsFinal, ambos boolean.
Pronto, agora eu consigo determinar se um status é inicial e se um status é final, e isso é suficiente para atender todas as regras expressas acima.
Essa abordagem serve para outros exemplos, como aquelas solicitações que dizem que o cliente X paga possui um comportamento especial. Crie um atributo que determine que esse cliente possui um comportamento específicos, ou extrapolando necessidades possíveis, crie estruturas inteiras(talvez novas classes, consequentemente tabelas) que expressem comportamento (que digam qual comportamento usar) e associe ao seu cliente.
Comparando abordagens
IsInitial e IsFinal
Vamos comparar as 2 abordagens. A primeira logo abaixo utiliza esses magic numbers, enquanto a segunda usa discriminadores de comportamento.
public Issue NewIssue() { return new Issue(){ Status = this.StatusRepository.GetStatus(1) }; } // Primeira Abordagem public IEnumerable<Issue> GetIssues(bool final) { if(final) return this.IssueRepository.GetIssues(it => it.Status.StatusId == 3 || it.Status.StatusId == 4 ); else return this.IssueRepository.GetIssues(it => it.Status.StatusId != 3 && it.Status.StatusId != 4 ); } // Segunda Abordagem public IEnumerable<Issue> GetIssues(bool final) { if(final) return this.IssueRepository.GetIssues(it => it.Status.Name == "Resolvido" || it.Status.Name == "Cancelado" ); else return this.IssueRepository.GetIssues(it => it.Status.Name != "Resolvido" && it.Status.Name != "Cancelado" ); }
Note que o que é considerado mágico são os números, a que se referem, qual o impacto desse número? O que acontece quando eu quiser adicionar um novo status. E mesmo que nunca queira, como eu testo esses comportamentos?
Há diversas formas de contornar o problema, mas o grande vilão é não pensar na melhor estratégia ao seu alcance. Nesse cenário real ao qual me refiro, temos total controle sobre o banco. Temos total capacidade e autonomia para alterar tabelas, criar novas, enfim: Temos total autonomia para tomar as melhores decisões em prol da manutenibilidade.
public Issue NewIssue() { return new Issue() { Status = this.StatusRepository.GetSingle(it => it.IsInitial) }; } public IEnumerable<Issue> GetIssues(bool final) { return this.IssueRepository.GetIssues(it => it.Status.IsFinal == final); }
Note que a complexidade ciclomática é significantemente reduzida. Essa estratégia me permite criar novos status, ao mero custo de um insert em banco, que por sua vez poderia ser gerido por uma UI (tela, página, view…).
O único cenário em que precisaria de refactoring é caso existisse a necessidade de 2 status iniciais, já que essa abordagem não suporta isso. Mas com toda a certeza a solução não seria realizar a identificação do status inicial com um mero IF por ID. Para a solução talvez fosse necessário adicionar outro atributo que discriminasse o contexto ao qual se refere.
Note também que forçosamente utilizo Single ao invés de First. Obrigando a existência de apenas 1 elemento e somente 1 elemento com a característica que preciso para status inicial. Eu prefiro que a aplicação pare com a criação do segundo status Inicial (que representa um erro para essa implementação, no momento) do que ter um comportamento duvidoso, onde o First pode entregar qualquer um dos 2 elementos e assim me causar também problemas, dessa vez nos dados, e dada a natureza do erro, um pouco mais difícil de fazer troubleshooting.
Conclusão
Existem diversas outras formas elegantes de resolver a questão, já resolvi usando outras estratégias menos intrusivas. Não cabe detalhar aqui, pois algumas precisam de um detalhamento que constrói toda uma narrativa a respeito da solução.
Eu preferi isolar um cenário super simples, quase banal e cotidiano para entregar a clareza no mindset e tentar incentivar você a olhar para os problemas do dia-a-dia de forma mais estratégica. Nós não somos pagos para escrever código burro, pelo menos não deveríamos. Passar por uma questão assim esses dias me fez refletir sobre a necessidade de escrever esse post.
Mas e você, vislumbra alternativas elegantes para o problema? Conta nos comentários! Será um prazer te escutar!
0 comentários