Ir para o conteúdo principal
Background Image

Como Identificar e Tratar Obsessão por Tipos Primitivos

··7 minutos·
Rafael Issao
Autor
Rafael Issao
Apaixonado por tecnologia, programação e inovação. Adoro compartilhar conhecimento e aprender coisas novas todos os dias.
Tabela de conteúdos

Você trabalha em um sistema de estoque e precisa implementar o conceito de data de validade.

A data de validade será importante para duas situações:

  • Descobrir qual o produto que vence antes para dispensá-lo antes do estoque
  • Descobrir quais produtos já vencerem para ser removidos do estoque

Você e sua equipe decidem colocar a nova propriedade na classe Produto.

Como você implementaria?

Se você pensou em utilizar uma classe Date, pode ser que você esteja criando um code smell chamado Obsessão por Tipos Primitivos!

Chamamos de Obsessão por Tipos Primitivos quando utilizamos tipos de dados oferecidos pela linguagem para simular um conceito da regra de negócio.

No exemplo acima seria utilizar um Date para simular uma data de validade.

Vamos entender por que pode ser um code smell e como tratar esse sintoma!

Primitivo mais complexo que o conceito
#

Vamos analisar se Date é uma representação interessante para data de validade.

Data de validade pode ser composta por:

  • Mês e ano: Nesta situação, o produto é válido até o final do mês
  • Dia, mês e ano

E deve ser comparável com:

  • Outra data de validade - Para descobrir qual o produto que vence antes
  • Data de hoje - Para descobrir se o produto está vencido

O código poderia ser algo assim:

package br.com.youready.article.d_2024_12_17.image1;

import java.util.Calendar;
import java.util.Date;

class ProdutoComDate {
    private final Date validade;

    private ProdutoComDate(Date validade) {
        this.validade = validade;
    }

    public static ProdutoComDate comValidade(int mes, int ano) {
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.YEAR, ano);
        calendar.set(Calendar.MONTH, mes - 1); // Lembrar que é zero-based index
        calendar.set(Calendar.DAY_OF_MONTH, calendar.getActualMaximum(Calendar.DAY_OF_MONTH));
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        return new ProdutoComDate(calendar.getTime());
    }

    public static ProdutoComDate comValidade(int dia, int mes, int ano) {
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.YEAR, ano);
        calendar.set(Calendar.MONTH, mes - 1); // Lembrar que é zero-based index
        calendar.set(Calendar.DAY_OF_MONTH, dia);
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        return new ProdutoComDate(calendar.getTime());
    }

    public boolean venceAntes(ProdutoComDate outroProduto) {
        return this.validade.before(outroProduto.validade);
    }

    public boolean vencido() {
        return this.validade.before(Calendar.getInstance().getTime());
    }
}

Primeira coisa que podemos observar é que precisamos de somente três inteiro, dia, mês e ano, para representar data de validade.

O objeto Date possui muito mais informações como, por exemplo, horas, minutos, locale, timezone e horário de verão.

Ou seja, se utilizarmos o Date para representar data de validade, estaremos trazendo uma complexidade desnecessária para o design!

E uma outra dificuldade é determinar como representar uma validade que tem somente mês e ano em um Date. Representamos com o último dia do mês? E se esse dia for exatamente o dia em que acontece o horário de verão e o sistema troca de data automaticamente?

Então o Date não seria um bom candidato para simular a data de validade.

Devemos criar uma abstração para o conceito de data de validade!

Primitivo parecido com o conceito
#

Podemos pensar em outra alternativa que é utilizar o LocalDate que possui somente dia, mês e ano:

package br.com.youready.article.d_2024_12_17.image2;

import java.time.LocalDate;
import java.time.YearMonth;

class ProdutoComLocalDate {
    private final LocalDate validade;

    private ProdutoComLocalDate(LocalDate validade) {
        this.validade = validade;
    }

    public static ProdutoComLocalDate comValidade(int mes, int ano) {
        return new ProdutoComLocalDate(YearMonth.of(ano, mes).atEndOfMonth());
    }

    public static ProdutoComLocalDate comValidade(int dia, int mes, int ano) {
        return new ProdutoComLocalDate(LocalDate.of(dia, mes, ano));
    }

    public boolean venceAntes(ProdutoComLocalDate outroProduto) {
        return this.validade.isBefore(outroProduto.validade);
    }

    public boolean vencido() {
        return this.validade.isBefore(LocalDate.now());
    }
}

O código fica mais simples!

Só de utilizarmos uma representação mais simples, obtemos um design mais interessante.

E será que precisamos abstrair o conceito de validade nessa situação?

A resposta é: depende!

Se você responder sim para qualquer uma das perguntas, você deve abstrair:

  • É um conceito importante na regra de negócio?
  • Pode sofrer alterações futuras?
  • Quer facilitar os testes?
  • Está com dúvida?

Se a resposta for não para todas as perguntas acima, podemos manter o código acima e refatorar quando necessário.

Por exemplo, se você sabe que, futuramente, seu sistema precisará lidar com produtos em fusos horários diferentes, é interessante já abstrair o conceito de data de validade:

Com isso, você pode alterar a estrutura interna de como funciona a data de validade sem impactar o resto do sistema. Melhoramos a coesão do código!

Exemplo de como podemos abstrair a data de validade:

package br.com.youready.article.d_2024_12_17.image3;

class Produto {
    private final DataValidade validade;

    private Produto(DataValidade validade) {
        this.validade = validade;
    }

    public static Produto comValidade(int mes, int ano) {
        return new Produto(DataValidade.de(mes, ano));
    }

    public static Produto comValidade(int dia, int mes, int ano) {
        return new Produto(DataValidade.de(dia, mes, ano));
    }

    public boolean venceAntes(Produto outroProduto) {
        return this.validade.venceAntes(outroProduto.validade);
    }

    public boolean vencido() {
        return this.validade.vencido();
    }
}

E a implementação da classe DataValidade pode ser feita utilizando o LocalDate, Date, inteiros e assim por diante.

Um exemplo com LocalDate:

package br.com.youready.article.d_2024_12_17.image4;

import java.time.LocalDate;
import java.time.YearMonth;

class DataValidade {
    private final LocalDate data;

    private DataValidade(LocalDate data) {
        this.data = data;
    }

    public static DataValidade de(int mes, int ano) {
        return new DataValidade(YearMonth.of(ano, mes).atEndOfMonth());
    }

    public static DataValidade de(int dia, int mes, int ano) {
        return new DataValidade(LocalDate.of(ano, mes, dia));
    }

    public boolean venceAntes(DataValidade validade) {
        return this.data.isBefore(validade.data);
    }

    public boolean vencido() {
        return this.data.isBefore(LocalDate.now());
    }
}

Outro exemplo com inteiros:

package br.com.youready.article.d_2024_12_17.image5;

import java.time.LocalDate;

class DataValidade {
    private final int dia, mes, ano;

    private DataValidade(int dia, int mes, int ano) {
        this.dia = dia;
        this.mes = mes;
        this.ano = ano;
    }

    public static DataValidade de(int mes, int ano) {
        return new DataValidade(99, mes, ano);
    }

    public static DataValidade de(int dia, int mes, int ano) {
        return new DataValidade(dia, mes, ano);
    }

    public boolean venceAntes(DataValidade validade) {
        return venceAntes(validade.dia, validade.mes, validade.ano);
    }

    public boolean vencido() {
        LocalDate agora = LocalDate.now();
        int dia = agora.getDayOfMonth();
        int mes = agora.getMonthValue();
        int ano = agora.getYear();
        return venceAntes(dia, mes, ano);
    }

    private boolean venceAntes(int outroDia, int outroMes, int outroAno) {
        if (this.ano < outroAno) {
            return true;
        } else if (this.ano == outroAno) {
            if (this.mes < outroMes) {
                return true;
            } else if (this.mes == outroMes) {
                return this.dia < outroDia;
            }
        }
        return false;
    }
}

O importante é que isolamos o conceito de data de validade em uma classe e tudo relacionado com o conceito está implementado em um único lugar.

Primitivo mais simples que o conceito
#

Agora precisamos implementar o preço de cada produto no sistema.

A primeira ideia poderia ser implementar o preço com BigDecimal.

Mas essa não é uma boa ideia.

É a situação onde o primitivo carece de estrutura para simular um conceito da regra de negócio.

O preço não é composto somente por número. Ele deve ter a moeda e o número de casas decimais.

Dependendo do produto, talvez queiramos utilizar duas casas decimais, ou mais:

E temos a questão da formatação também:

Para implementar todos estes comportamentos que fazem parte do preço, o ideal é sempre criar uma abstração para o conceito.

Conclusão
#

Existem situações onde primitivos são suficientes e outras não.

Uma heurística que uso com frequência para determinar se devo criar uma abstração para o conceito:

  • Primitivo é mais complexo que o conceito - Criar uma abstração
  • Primitivo é mais simples que o conceito - Criar uma abstração
  • Primitivo é parecido com o conceito - Criar uma abstração se responder sim para alguma das perguntas. Não criar se eu tiver certeza que é não para todas as perguntas

Na minha experiência, são poucas as situações que não preciso criar uma abstração.

Mas mesmo assim prefiro sempre pensar e refletir antes de criar uma abstração para não entrar no modo automático e ser dogmático.

E você? Será que não está dependendo muito de primitivos e criando classes utilitárias (Os famosos ***Utils) para tratar os comportamentos dos conceitos da regra de negócio do seu sistema?

Se você tiver dúvidas, sugestões ou até correções, sinta-se à vontade para comentar ou falar diretamente comigo!

🥒🥒 Até o próximo artigo! 🥒🥒