C# - Resolvendo a obsessão por tipos primitivos


 Este artigo apresenta uma abordagem para resolver o problema do code smell conhecido como primitive obsession ou obsessão por primitivos.


A palavra obsessão significa - apego exagerado. motivação irresistível - e hoje vamos apresentar a obsessão por primitivos que se manifesta quando em um projeto temos o uso exagerado e não justificado de tipos primitivos de forma que os valores dos tipos primitivos controlam a lógica do objeto.

A obsessão por primitivos

As classes são apenas modelos burros até que sejam definidas com o comportamento adequado,  e , o  comportamento de uma classe é definido por suas propriedades, campos e métodos. Quando usamos tipos primitivos para definir os membros da classe isso por si só não é um problema quando não existe um exagero nesta abordagem.

Mas quando a utilização de tipos primitivos aumenta exageradamente e eles são definidos em locais diferentes do código acabamos tendo código duplicado sem perceber, ,e  chegamos no code smell conhecido como primitive obsession.

Assim podemos elencar algumas características da obsessão primitiva :

1- Uso de tipos primitivos em excesso;
2- Uso de constantes ou constantes string para nomes de campo;
3- Uso do tipo primitivo numérico para instruções condicionais ou instruções de validação;

Apresentando o problema

Vejamos um exemplo para ilustrar o problema.

Vamos definir uma classe Funcionario com um construtor e uma propriedade Nome do tipo string:

 public class Funcionario
 {
        public Funcionario(string nome)
        {
            Nome = nome;
        }
        public string Nome { get; set; }
 }

Vamos supor que após algum tempo esta classe sofreu alterações com inclusão de novas propriedades:

    public class Funcionario
    {
        public Funcionario(int id, string nome, string sobrenome, string celular, string inss)
        {
            ID = id;
            Nome = nome;
            Sobrenome = sobrenome;
            Telefone = celular;
            INSS = inss;
        }
        public string Nome { get; set; }
        public string Sobrenome { get; set; }
        public int ID { get; set; }
        public string Telefone { get; set; }
        public string INSS { get; set; }
    }

Os membros Telefone e INSS foram definidos como do tipo string e ainda temos os seguintes requisitos a cumprir :

  1. O membro Telefone esta definido como uma string.  É preciso extrair o código de área do número informado.
     

  2. O membro INSS também é uma string.  É necessário extrair os últimos quatro dígitos do INSS de um determinado número de previdência social.

Note que para realizar a extração do código de área e/ou dos últimos 4 dígitos do membro INSS teremos que incluir uma lógica extra no tipo.

Assim, a título de exemplo vamos definir o código para extrair o número de área do Telefone criando o método GetCodigoArea():

    public string GetCodigoArea()
    {
            var codigoArea = "";
            var telefone = Telefone;
            int index = telefone.LastIndexOf("-", StringComparison.Ordinal);
            if (index > 0 && index < telefone.Length)
                 codigoArea = telefone.Substring(0, index);
            
            return codigoArea;
    }

A seguir vamos definir o código para extrair os 4 últimos números do INSS criando o método GetDigitosINSS():

   public string GetDigitosINSS()
    {
         var index = INSS.LastIndexOf("-", StringComparison.Ordinal);
        return index > 0 && index < INSS.Length
              ? INSS.Substring(index + 1, INSS.Length - index + 1)
                 : INSS;
   }

Criamos assim dois novos métodos, e, se esses métodos forem incluídos na classe Funcionario os requisitos serão cumpridos.

Mas se incluirmos esses métodos na classe Funcionario estamos adicionando lógica de validação específica primitiva à classe e isso nos traz os seguintes problemas:

1-  A lógica de extração e da obtenção do código de área será propriedade da classe Funcionario;

2- Se a lógica for necessária em outras partes da sua aplicação, o código terá que ser duplicado. Você acabará instanciando a classe Funcionario para que possa usar o método GetCodigoArea() ou GetDigitosINSS(), o que é absolutamente desnecessário e não recomendado.

Resolvendo o problema da obsessão por primitivos

Como podemos resolver o problema de forma satisfatória e de forma que esteja aderente às boas práticas ?

Aqui podemos usar as técnicas sugeridas por Martin Fowler no seu livro : Refactoring: Improving the Design of Existing Code”.

Com base nelas temos que a obsessão primitiva manifestada acima pode ser resolvida da seguinte forma:

As técnicas acima se concentram na substituição do tipo primitivo por um ValueObject, uma Classe ou uma SubClasse;

Toda validação ou lógica de extração se tornará parte do ValueObject, da Classe ou da SubClasse, e, isso evitará a duplicação de código.

Assim para resolver o problema vamos criar duas novas classes : 

  1. PrevidenciaSocial - Para tratar o com o INSS e aqui vamos usar o método GetDigitosINSS();

  2. Contato  - Para tratar com o Telefone e aqui vamos usar o método GetCodigoArea();

1- PrevidenciaSocial()

using System;
namespace C_ObsessaoPorPrimitivos
{
    public class PrevidenciaSocial
    {
        public PrevidenciaSocial(string inss)
        {
            INSS = inss;
        }
        public string INSS { get; }
        public string GetDigitosINSS()
        {
            var index = INSS.LastIndexOf("-", StringComparison.Ordinal);
            return index > 0 && index < INSS.Length
                   ? INSS.Substring(index + 1, INSS.Length - index + 1)
                   : INSS;
        }
    }
}

Criamos o tipo PrevidenciaSocial e incluímos o método GetDigitosINSS() nesta classe.

1- Contato

using System;
namespace C_ObsessaoPorPrimitivos
{
    public class Contato
    {
        public Contato(string numero)
        {
            Telefone = numero;
        }
        public string Telefone { get;  }
        public string GetCodigoArea()
        {
            var codigoArea = "";
            var telefone = Telefone;
            int index = telefone.LastIndexOf("-", StringComparison.Ordinal);
            if (index > 0 && index < telefone.Length)
                codigoArea = telefone.Substring(0, index);
            return codigoArea;
        }
    }
}

 

Dessa forma criamos dois novos tipos que serão usados na classe Funcionario :

    public class Funcionario
    {
        public Funcionario(int id, string nome, string sobrenome, Contato celular, PrevidenciaSocial inss)
        {
            ID = id;
            Nome = nome;
            Sobrenome = sobrenome;
            Contato = celular;
            PrevidenciaSocial = inss;
        }
        public string Nome { get; set; }
        public string Sobrenome { get; set; }
        public int ID { get; set; }
        public Contato Contato { get; set; }
        public PrevidenciaSocial PrevidenciaSocial { get; set; }
    }

Na figura a seguir temos as classes que compõe o modelo :

Com isso evitamos a duplicação de código e expressamos o domínio de uma forma mais rica e robusta onde podemos realizar uma validação mais consistente e ainda poderemos estender os tipos criados com novos comportamentos sem ter que modificar a classe Funcionario.

Aqui Contato e PrevidenciaSocial podem ser considerados como ValueObjects e possuem como característica não possuírem uma identidade, serem imutáveis e representarem um valor.

Nota: A plataforma .NET possui tipos embutidos que encapsulam tipos primitivos e seus métodos como a estrutura DateTime.

E estamos conversados...

"Melhor é o que tarda em irar-se do que o poderoso, e o que controla o seu ânimo do que aquele que toma uma cidade."
Provérbios 16:32

Referências:


José Carlos Macoratti