C# -  Obsessão por primitivos (revisitado)


 Hoje vamos retornar ao assunto obsessão por primitivos recordando este importante conceito.

O termo 'obsessão por primitivos' na linguagem C# refere-se à preferência por tipos de dados primitivos em detrimento de tipos de dados mais complexos ou personalizados.



Na linguagem C#, os tipos de dados primitivos incluem tipos numéricos (como int, float e double), tipos booleanos (bool) e tipos de caracteres (char). Esses tipos são considerados primitivos porque eles são tipos de dados básicos que são implementados diretamente pelo compilador e pela máquina virtual  sem a necessidade de bibliotecas ou código adicional.

Embora a linguagem C# permita a criação de tipos de dados personalizados (como classes e estruturas), a obsessão por primitivos defende que, sempre que possível, é melhor usar os tipos primitivos, devido à sua eficiência e simplicidade.

Essa abordagem pode ser útil em cenários de alto desempenho, onde o tempo de execução é um fator crítico. No entanto, em outros casos, pode ser mais apropriado usar tipos de dados personalizados para melhorar a legibilidade, modularidade e manutenção do código.

Um exemplo de código que evidencia a obsessão por primitivos na linguagem C# seria a utilização de tipos numéricos primitivos em vez de tipos personalizados para representar unidades de medida.

Por exemplo, em vez de criar uma classe "Tempo" com propriedades "Horas", "Minutos" e "Segundos", um programador que segue a obsessão por primitivos poderia optar por usar apenas um tipo numérico para representar o tempo total em segundos:


 int tempoTotalSegundos = 3600;
// representa 1 hora    
 

Embora essa abordagem seja mais simples e direta, ela pode tornar o código menos legível e mais suscetível a erros. Além disso, ela não permite a inclusão de informações adicionais, como a data em que o tempo foi registrado, por exemplo.

Nesse caso, seria mais apropriado criar uma classe personalizada para representar o tempo, como no exemplo a seguir:

public class Tempo
{
  
public int Horas { get; set; }
  
public int Minutos { get; set; }
  
public int Segundos { get; set; }

  
public Tempo(int horas, int minutos, int segundos)
   {
      Horas = horas;
      Minutos = minutos;
      Segundos = segundos;
   }

  
public override string ToString()
   {
    
return $"{Horas}h {Minutos}m {Segundos}s";
   }
}

O código a seguir permite definir um tempo e realizar a exibição no console:


Tempo tempoRegistrado =
new Tempo(1, 0, 0);

Console.WriteLine(tempoRegistrado.ToString());
 

Resultado: 

Nesse exemplo, a classe "Tempo" permite a inclusão de informações adicionais e torna o código mais legível e fácil de entender. Embora seja um pouco mais complexo do que o exemplo anterior, essa abordagem é mais flexível e pode ser mais adequada em muitos casos.

Vejamos outro exemplo onde é muito comum o uso de tipos primitivos.

A seguir temos o código da classe Endereco onde temos o uso de tipos primitivos para representar o Cep, o Local e o Pais :

public class Endereco
{
  
public string Cep { get; set; }
  
public string Local { get; set; }
  
public string Pais { get; set; }

  
public void ValidarCep(string cep)
   {}
}

Para refatorar a classe Endereco de forma a não usar primitivos para representações inadequadas, podemos usar tipos personalizados para representar informações mais complexas e específicas.

Por exemplo, podemos criar uma classe CEP para representar o CEP do endereço, em vez de usar uma string genérica:

public class CEP
{
    public string Numero { get; private set; }
    public CEP(string numero)
    {
        Validar(numero);
        Numero = numero;
    }
    private void Validar(string numero)
    {
        if (string.IsNullOrEmpty(numero) || numero.Length != 8 || !int.TryParse(numero, out int _))
        {
            throw new ArgumentException("CEP inválido.");
        }
    }
    public override string ToString()
    {
        return Numero;
    }
}

Com isso podemos ajustar o código da classe Endereco usando o tipo CEP definido no lugar da string:

public class Endereco
{
  public CEP Cep { get; set; }
 
public string Local { get; set; }
 
public string Pais { get; set; }
}

Nesse exemplo, a classe CEP encapsula a lógica de validação do CEP e impede a criação de instâncias inválidas. Além disso, ela pode ser facilmente reutilizada em outras partes do código, caso necessário.

Também podemos usar tipos personalizados para representar outras informações do endereço, como o país e o local, se necessário. Por exemplo:

1- Pais

public class Pais
{
    public string Nome { get; private set; }
    public Pais(string nome)
    {
        Validar(nome);
        Nome = nome;
    }
    private void Validar(string nome)
    {
        if (string.IsNullOrEmpty(nome))
        {
            throw new ArgumentException("Nome do país inválido.");
        }
    }
    public override string ToString()
    {
        return Nome;
    }
}

2- Local

public class Local
{
    public string Nome { get; private set; }
    public Local(string nome)
    {
        Validar(nome);
        Nome = nome;
    }
    private void Validar(string nome)
    {
        if (string.IsNullOrEmpty(nome))
        {
            throw new ArgumentException("Nome do local inválido.");
        }
    }
    public override string ToString()
    {
        return Nome;
    }
}

Podemos agora definir a classe Endereco da seguinte forma :

public class Endereco
{
    public CEP Cep;
    public Local Local;
    public Pais Pais;
    public Endereco(CEP cep, Local local, Pais pais)
    {
        Cep = cep;
        Local = local;
        Pais = pais;
    }
    public override string ToString()
    {
        return $"CEP: {Cep.Numero}, Local: {Local.Nome}, País: {Pais.Nome}";
    }
}

Agora a classe Endereco usa as classes CEP, Loal e Pais que encapsulam a validação e a representação de informações específicas do endereço, tornando o código mais legível e coeso.

A seguir vamos criar dois endereços e exibir no console:

// Exemplo de criação de objetos Endereco
var
cep1 = new CEP("12345678");
var
local1 = new Local("Rua A, 123");
var
pais1 = new Pais("Brasil");

var endereco1 = new Endereco(cep1, local1, pais1);

var cep2 = new CEP("54321876");
var
local2 = new Local("Rua B, 456");
var
pais2 = new Pais("Estados Unidos");

var endereco2 = new Endereco(cep2, local2, pais2);

// Exibindo os objetos criados
Console.WriteLine(endereco1);
Console.WriteLine(endereco2);
 

Desta forma podemos concluir que quando utilizamos apenas tipos primitivos, temos algumas desvantagens, como:

Por outro lado, quando utilizamos classes e tipos personalizados para representar valores e objetos, temos algumas vantagens, como:

Portanto, utilizar classes e tipos personalizados em vez de tipos primitivos pode trazer diversas vantagens em relação à legibilidade, reusabilidade e evolução do código em C#.

Naturalmente você deve usar o bom senso e considerar o cenário e o contexto do seu código para decidir quando usar ou não tipos primitivos e quais os benefícios que isso pode te trazer.

E estamos conversados ...

"Há um caminho que ao homem parece direito, mas o fim dele são os caminhos da morte."
Provérbios 14:12

Referências:


José Carlos Macoratti