Todas as linguagens de programação possuem estruturas de dados primitivas (inteiro, string, byte e assim por diante), das quais todas as demais deveriam descender. Porém, geralmente nós utilizamos esses tipos básicos para representar dados complexos como nome, número de documentos, períodos de tempo. Essa prática pode ser tão problemática que inventaram até mesmo um nome pra ela: Obsessão por tipos primitivos (primivite obsession). O que isso quer dizer o porque essa prática pode ser problemática é o que vamos descobrir a seguir.
Mas o que é um Code Smell?
Eu gosto demais desses nomes “inventados” para catalogar problemas de software e Code Smell está entre os meus preferidos. Ele é muito auto explicativo! Veja: eu até posso imaginar uma voz lá no fundo dizendo: “Hmmm… Isso não está me cheirando bem. Será que não existe uma forma melhor de implementar isso?”. E é exatamente essa a ideia que o Code Smell quer traduzir. Não há efetivamente nada de errado com o que você está fazendo. Se rodar o código, ele até compila e passa nos testes mas… Isso não me parece certo/Não cheira bem.
Enquanto faço code review, já ouvi inúmeras vezes reclamações do tipo: “Mas hei! O código está funcionando! Deixa assim!”. A verdade é que “funcionar” é o mínimo que o seu código pode fazer. Afinal, se ele não estiver funcionando, porque você abriria o Pull Request? Escrever código que funcione apenas hoje é fácil. O desafio real está em escrever código que funcione pelo maior tempo viável.
“Funcionar” é o mínimo que o seu código pode fazer. O desafio real está em escrever código que funcione pelo maior tempo viável.
São raras as situações em que você escreveria um algoritmo, hoje, já pensando em descartá-lo amanhã. Em geral todo código passará por manutenções e extensões no futuro. E é aqui que o conceito de maior tempo viável entra. Ele está ligado intimamente com o equilíbrio entre qualidade e custo da entrega (que vai além do custo hora/pessoa). Um código perfeito, além de não ser viável financeiramente, não existe! E na busca pelo equilíbrio entre custo e qualidade, fatalmente vamos deixar passar algumas coisas.
Por isso é importante um catalogo de code smells conhecidos. Ele lança uma lupa sobre “suspeita”, nos diz quais são as consequências, caso nós optemos por insistir no “erros” e ainda nos dá dicas de como refatorar. Nós vamos abordar outros code smells aqui no blog, mas enquanto isso, você pode acessar o site Refactoring Guru. Lá você encontrará um rico catálogo de code smells, além de outros assuntos sobre o tema de qualidade de código.
Obsessão por tipos primitivos (primitive obsession)
Vamos supor o seguinte cenário: Você precisa criar uma classe que represente um funcionário, para que você possa saber o período em que uma pessoa trabalhou em uma empresa. Logo, você vai precisar das informações:
- Nome da pessoa;
- Documento (CPF);
- Quando ela entrou na empresa;
- Quando ela saiu da empresa (se saiu);
Como você escreveria esse código? Talvez algo parecido com isso:
public class Funcionario
{
public string Nome { get; set; }
public string Cpf { get; set; }
public DateTime ContratadoEm { get; set; }
public DateTime? DemitidoEm { get; set; }
}
Você pode até dizer que esse código possui outros tantos problemas (que tal dizer aqui nos comentários?), mas vamos focar nesse que é o tema do artigo: Essa classe é um ótimo exemplo do code smell: obsessão por tipos primitivos (primitive obsession). Perceba que todas as propriedades da classe pertencem a um tipo pré-definido da linguagem. Sim! DateTime
e string
não são tipos primitivos, mas são tipos básicos da linguagem. E sim: a utilização deles também pode caracterizar o code smell que estamos abordando.
Mas afinal, qual o problema?
Se analisarmos bem cada uma das informações, vamos perceber que todas elas são tipos compostos. Vejamos:
Nome
: Todo nome é composto por Nome e Sobrenome. No Brasil, usualmente o Sobrenome é composto pelos sobrenomes da mãe e do pai respectivamente. Alguns sobrenomes podem conter numerais romanos também, ou qualificadores como Neto, Júnior, Filho. E a forma como representamos um nome também muda de acordo com a cultura. Em muitos países, por exemplo, o último sobrenome é como a pessoa usualmente é chamada e aparece primeiro. No Brasil, não damos bola pra isso.
CPF
: É composto por 9 dígitos, agrupados de 3 em três, separados por ponto (.). Existem mais outros dois dígitos que são “dígitos verificadores”, ou seja, servem para validar a cadeia inteira de informação.
Contratado/Demitido
: Estas propriedades estão intimamente ligadas ao período em que a pessoa está (ou esteve) trabalhando na empresa. Embora a classe DateTime
já faça a maior parte do trabalho bruto de validar uma data (nada de 30 de fevereiro), ainda tem o comportamento de Período não-fechado que precisa ser implementado. Ou seja: DemitidoEm
(fim do período) pode ser nulo (período não-fechado) mas jamais poderá ser anterior ao ContratadoEm
.
Diga a verdade: Você já tinha reparado em toda a complexidade envolvendo esses dados tão comuns? E quero deixar claro que ainda não esgotamos todas as complexidades. Por exemplo: Dependendo da cultura, a propriedade Nome
deve ser ordenada de uma forma diferente.
Se você só possui essa informação em um lugar, tudo bem: Você pode fazer toda essa implementação na classe de funcionários. Mas eu tenho certeza absoluta que você possui outras estruturas de dados que contenham a todas essas informações ou semelhantes. Período, com certeza, é uma que você deve ter bastante no seu código. E as validações podem estar igualmente espalhadas também.
Vamos supor que você está armazenando o CPF como inteiros (afinal, aceita apenas dígitos, não é mesmo?). Mas o Governo do país determinou que agora o CPF poderá conter letras também. Calcule o esforço para refatorar todo o sistema, replicando o código de validação em todas as classes que você utiliza o CPF. Pode chorar agora. Eu espero você.
Então tudo tem que ser um tipo?
Sendo bastante direto? Idealmente sim. Mas isso não me parece viável. Existe o problema da codificação e da manutenção em si. Afinal, o que lhe parece mais fácil:
Isso:
string nome = "Um nome qualquer";
Ou isso:
Nome nome = new Nome("Um nome qualquer");
Lembra que você deveria controlar – muito bem – o tempo de vida dessas classes, para não criar uma pressão desnecessária no Garbage Collector. E sim, talvez a raiz da resposta para pergunta “Então tudo tem que ser um tipo?” esteja na palavra “necessário”.
Por mais que o tipo Nome
tenha toda a complexidade que levantamos, ele é relevante para o domínio que estamos implementado? Se a resposta for não e o que queremos é apenas um conjunto de caracteres para saber como chamamos a pessoa, talvez um nome apenas precise ser representado por uma string
. E está tudo bem. Ao tomarmos essa decisão, estamos cientes do quanto isso nos custará no futuro: além de escrever o tipo, também fazer as refatorações necessárias para que o sistema continue funcionando.
Quanto ao CPF, apesar da regra não ser relevante a qualquer domínio (a não ser que vc esteja implementando pra Justifica Federal), o comportamento dele é importante. Pra você tanto faz como o CPF será apresentado. O importante é que ele seja válido. O mesmo pode ser dito do período: ele precisa ser válido. Particularmente acredito que criar tipos específicos para esse caso, não seja algo tão caro e que pode agregar valor.
Refatorando
Considerando as mudanças, nossa classe poderia ficar da seguinte maneira:
public class Funcionario
{
public string Nome { get; set; }
public Cpf Cpf { get; set; }
public Periodo Contratacao { get; set; }
}
Agora você pode pensar – e com razão – que apesar do ganho alcançado, removendo parte dos tipos primitivos, também houve uma perda significativa: O código ficou mais verboso. Para criar uma instância da classe Funcionario
, você teria que escrever algo como:
var jose = new Funcionario
{
Nome = "José",
Cpf = new Cpf("287.312.060-69"),
Contratacao = new Periodo(new DateTime("2021-07-05")),
};
A leitura ficou mais confortável (Contratacao = new Period(...)
é lindo!), mas você precisa escrever mais código! Se você utiliza frameworks para fazer o mapeamento à partir do banco de dados ou de um json, você pode ter problemas com conversão. Como resolver esses problemas? Isso é o que veremos nos próximos artigos.
Durante o design de software – assim como na arquitetura – a avaliação de trade-offs é constante, justamente porque cada escolha é também uma renúncia. Praticamente é impossível a gente “só ganhar” com uma solução, pois sempre haverá uma perda. Ter controle sobre essas perdas e minimizar o impacto dessas renúncias é como passar desodorante: Você sabe que eventualmente ele vai vencer, mas se tudo der certo, isso não acontecerá antes do próximo banho.