Superando a obsessão por tipos primitivos

Este capítulo possui uma versão mais recente:

The most damaging phrase in the language is.. it’s always been done this way
Grace Hopper

Quase todas as linguagens de programação suportam, em maior ou menor escala, tipos primitivos. Em C#, por exemplo, temos tipos como string, long, double, int, bool, etc. Esses tipos servem como “blocos fundamentais” para a construção de representações mais complexas.

Python e tipos primitivos

Python não tem tipos primitivos, pelo menos, implementados da mesma forma que outras linguagens de programação. Afinal, na linguagem, mesmo tipos como int, bool são objetos. Entretanto, para os fins do propósito desse capítulo, podemos entender esses tipos “fundamentais” para Python, como primitivos, sem prejuízo de entendimento (apenas de rigor)

Em linguagens procedurais, é comum criar “rotinas no entorno” dos tipos primitivos. Por exemplo, a representação de um “funcionário” em sistemas de gestão, desenvolvido em linguagem procedural, não é mais que um conjunto de variáveis com tipos primitivos, eventualmente agrupados em alguma “estrutura”.

typedef struct {
    float Weight;
    int Age;
    float Height;
} Person;

Os valores destas variáveis, ainda falando de programação procedural, são manipulados e validados por rotinas distintas (funções), não raro, “acopladas” com uma funcionalidade do software – um CPF, assim, geralmente é armazenado em uma variável string, validada em funções acionadas nas rotinas que manipulem essa informação.

Já no paradigma orientado a objetos, há o encapsulamento – dados e comportamento não devem estar separados, mas combinados em objetos. Infelizmente, entretanto, há uma certa “obsessão” por continuar usando tipos primitivos de maneira semelhante a que acontece no paradigma procedural, levando a códigos que pecam por baixa coesão.

Outro problema comum de implementação, característico da obsessão de tipos primitivos, é a utilização de valores constantes numéricos como “códigos de retorno”. Muito mais interessante é a utilização de enumeradores.

Bons designs orientados a objetos evitam a obsessão por tipos primitivos.

Um exemplo de obsessão por primitivos

No exemplo que segue, temos uma implementação, comum, obsessiva por tipos primitivos.

public class Employee
{
  public string Id { get; set; }
  public string Name { get; set; }
  
  string _cpf;
  public string Cpf 
  { 
    get
    {
      return _cpf;
    } 
    set
    {
      if (Utils.CheckCpf(value))
      {
        throw new ArgumentException(“...”);
      }
      _cpf = value;
    } 
  }
  public decimal Salary { get; set; }
}

No código de exemplo, o “tipo” associado a propriedade CPF é string. Além disso, podemos observar a utilização de uma função, CheckCpf, estática, implementada em uma “biblioteca” de propósito geral, nos mesmos moldes que ocorreria em um linguagem adequada ao paradigma procedural.

Substituindo tipos primitivos

A superação da obsessão por tipos primitivos começa pelo reconhecimento de elementos mais “sutis” na modelagem, como a representação de valores simples, também como objetos. Em DDD, esses objetos são identificados como value objects.

Criando tipos mais expressivos (que tipos primitivos)

A superação da “obsessão” por tipos primitivos começa pelo exercício de combinar estado e comportamento (por exemplo validações), em um objeto apropriado.

public struct Cpf
{
  private readonly string _value;

  public Cpf(string value)
    => _value = value;

  public override string ToString()
    => _value;

  public static implicit operator Cpf(string value)
    => Parse(value);

  public bool IsValid
  {
     get 
     {
        // validation logic;
     }
  }

  public bool IsEmpty => (_value != null);
}

Na implementação de exemplo, deve chamar a atenção a utilização do modificador readonly no atributo que representa o estado. A ideia é que, uma vez atribuído um valor a este atributo, ele não possa ser modificado. Objetos para representar value objects são naturalmente imutáveis, afinal, eles são “expressão de um valor”.

Ainda no código de exemplo, também deve chamar a atenção o fato de construirmos o tipo como uma struct. As características de objetos definidos como struct, em C#, tais como comparação por valor e alocação default na stack (em oposição a class que define objetos que serão alocados na heap), eliminam potenciais impactos de performance e legibilidade. Também é interessante verificar a implementação de conversores implícitos para tipos primitivos, facilitando atualização das bases de código.

Finalmente, é interessante observar o encapsulamento em cena. Aliás, o estado (valor do Cpf), fica escondido no código de exemplo. A “visão externa”, quando acontece, é determinada pelo método ToString(). Com alguma criatividade, podemos ampliar essa “visão externa”, por exemplo, criando métodos distintos para retornar um Cpf formatado ou não formatado. Afinal, o valor é o mesmo, exceto pela representação.

Adoção de Parse/TryParse

Uma vez que resolvemos criar objetos, é interessante os modelar seguindo alguns padrões vigentes. Em .NET, por exemplo, é comum a utilização do padrão Parse/TryParse.

public struct Cpf
{
  private readonly string _value;

  private Cpf(string value)
    => _value = value;

  public override string ToString()
    => _value;

  public static implicit operator Cpf(string value)
    => Parse(value);
	
  public static Cpf Parse(string value)
    => (TryParse(value, out var result))
      ? result
      : throw new System.ArgumentException(nameof(value));
 	
  public static bool TryParse(string value, out Cpf result)
  {
    if (string.IsNullOrEmpty(value) || value.Length != 11)
    {
      result = default(Cpf);
      return false;
    }
    result = new Cpf(value);
    return true;
  }
  
  public bool IsEmpty => (_value != null);
}

A classe Employee, agora, pode ser muito mais enxuta.

public class Employee
{
  public string Id { get; set; }
  public string Name { get; set; }
  public Cpf Cpf {get; set;}
  
}

Indo “mais longe” com sobrecarga de operadores

Algumas linguagens com suporte ao paradigma orientado a objetos, como C#, oferecem suporte a sobrecarga de operadores.

public readonly struct Fraction
{
  private readonly int _numerator;
  private readonly int _denominator;

  public Fraction(int numerator, int denominator)
  {
    if (denominator == 0)
    {
      throw new ArgumentException("Denominator cannot be zero.", nameof(denominator));
    }
    _numerator = numerator;
    _denominator = denominator;
  }

  public static Fraction operator +(Fraction a) 
    => a;

  public static Fraction operator -(Fraction a) 
    => new Fraction(-a.num, a.den);

  public static Fraction operator +(Fraction a, Fraction b)
    => new Fraction(
         a._numerator * b._denominator + b._numerator * a._denominator, 
         a._denominator * b._denominator
       );

  public static Fraction operator -(Fraction a, Fraction b)
     => a + (-b);
  
  // ...

  public static implicit operator Fraction(int value)
    => new Fraction(value, 1);

  public override string ToString() => $"{_numerator} / {_denominator}";

  public double ToDouble()
    => _numerator / (double) _denominator;
}

Java não tem suporte a sobrecarga de operadores.

Tornando o código mais expressivo com enumerações

É comum, em determinados contextos, utilizarmos numéricos representando valores constantes significativos. Por exemplo, dias da semana.

const int SUNDAY    = 0;
const int MONDAY    = 1;
const int TUESDAY   = 2;
const int WEDNESDAY = 3;
const int THURSDAY  = 4;
const int FRIDAY    = 5;
const int SATURDAY  = 6;

A adoção de constantes é um recurso poderoso, adotado por programadores utilizando linguagens procedurais, para substituir “valores mágicos” (valores adicionados diretamente no código, sem significado evidente).

Adotando enumerações

Em linguagens orientadas a objetos, uma abordagem ainda mais “expressiva” é consolidar todos esses valores em uma enumeração.

System.Console.WriteLine(DaysOfWeek.Sunday);

enum DaysOfWeek
{
 Sunday,
 Monday,
 Tuesday,
 Wednesday,
 Thursday,
 Friday,
 Saturday
}

Enumerações são tipos especiais de classes que são fáceis de definir e dão a ideia de “conjunto”.

Evenutalmente, uma enumeração poderá ter conversão facilitada através da especificação de valores numéricos de um determinado tipo.

enum DaysOfWeek : int
{
 Sunday    = 0,
 Monday    = 1,
 Tuesday   = 2,
 Wednesday = 3,
 Thursday  = 4,
 Friday    = 5 ,
 Saturday  = 6
}

Suportando combinações de valores com enumeradores de bits

Eventualmente, enumeradores podem ser utilizados como “mapas de bits” de forma a permitir atribuições de valores múltiplos.

[Flags]

enum DaysOfWeek : int
{
 Sunday    = 0b_0000_0001,  // 1
 Monday    = 0b_0000_0010,  // 2,
 Tuesday   = 0b_0000_0100,  // 4,
 Wednesday = 0b_0000_1000,  // 8,
 Thursday  = 0b_0001_0000,  // 16,
 Friday    = 0b_0010_0000,  // 32,
 Saturday  = 0b_0100_0000,  // 64,
 Weekend   = Saturday | Sunday
}

A utilização de mapas de bits é um artifício poderoso para suportar “combinação de valores” e, potencialmente, reduzir carga de armazenamento e transporte de dados.

Antes de avançar…

Neste capítulo curto identificamos e mostramos alternativas para “superar” a obsessão por tipos primitivos.

A adoção das técnicas indicadas aqui, permite a construção de códigos mais flexívei, fáceis de entender. e organizar. Afinal, toda as operações relacionadas a um determinado dado ficam em um único lugar.
0
Considerações?x

Na prática, a adoção de encapsulamento dificulta a duplicação. A utilização de enumerações torna o código mais expressivo.

Recomendo a você considerar e implementar outros exemplos de tipos, para valores que, no paradigma procedural, costumam ficar “dispersos” em atributos e funções desacrupadas.

Compartilhe este capítulo:

Compartilhe:

Comentários

Participe da construção deste capítulo deixando seu comentário:

Inscrever-se
Notify of
guest
2 Comentários
Oldest
Newest Most Voted
Feedbacks interativos
Ver todos os comentários
Isaac Roque Sartori Junior
Isaac Roque Sartori Junior
3 anos atrás

seria interessante colocar o “[Flags]” dentro do código no texto “suportando combinações…˜

Abimael Andrade
Abimael Andrade
3 anos atrás

Conteúdo muito rico, obrigado por compartilhar.

Elemar Júnior

Fundador e CEO da EximiaCo, atua como tech trusted advisor ajudando diversas empresas a gerar mais resultados através da tecnologia.

Mentorias

para arquitetos de software

Imersão, em grupo, supervisionada por Elemar Júnior, onde serão discutidos tópicos avançados de arquitetura de software, extraídos de cenários reais.

ElemarJúnior

Fundador e CEO da EximiaCo, atua como tech trusted advisor ajudando diversas empresas a gerar mais resultados através da tecnologia.

TECH

&

BIZ

-   Insights e provocações sobre tecnologia e negócios   -   

55 51 9 9942 0609  |  me@elemarjr.com

55 51 9 9942 0609  |  me@elemarjr.com

bullet-1.png

55 51 9 9942 0609  |  me@elemarjr.com

2
0
Quero saber a sua opinião, deixe seu comentáriox
()
x