Home Comunidade Eventos YouTube Networks Desafios // produtos Academy News CTI | CVE Contato

02 - Objeto de Valor (Value Object)

#ddd#imutabilidade#validacao#value-object
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione video: https://youtu.be/... no frontmatter da nota.

Objeto de Valor (Value Object)

O objeto de valor é um objeto de domínio definido pelos seus atributos, sem identidade própria, e
que trata qualquer combinação de valores como intercambiável com outra equivalente. Dois valores de
dinheiro com o mesmo montante e a mesma moeda são o mesmo valor, independentemente de onde foram
criados ou de quantas referências apontam para eles. Suas três propriedades fundamentais são a
imutabilidade, a igualdade por valor e a validação no momento da criação, que juntos garantem que
nenhuma instância inválida circule pelo sistema. Em produção, a adoção consistente de objetos de
valor elimina uma família inteira de defeitos: montantes negativos gravados em banco, CPF mal
formatado aceito em um endpoint e corrigido em outro, e moedas misturadas em somas que só explodem
no fechamento contábil.

Imutabilidade como contrato

Um objeto de valor nunca muda de estado: qualquer operação que altere seu conteúdo retorna uma nova
instância com o resultado, e a original permanece intacta. Esse contrato tem consequências práticas
diretas em produção. Referências podem ser compartilhadas entre objetos e threads sem cópia
defensiva, porque ninguém pode mudar o que está do outro lado. Caching fica seguro, pois um valor
devolvido por consulta nunca é alterado por quem o recebeu. Debug e auditoria ficam mais simples,
já que o valor observado em um ponto do tempo é o mesmo observado depois. O custo é a alocação de
novas instâncias em operações frequentes, que em linguagens modernas é quase sempre inferior ao
custo de caçar mutações não intencionais. A regra prática é não expor setters, declarar todos os
campos como finais e criar métodos que devolvem novas instâncias, como adicionar e aplicarDesconto
em um valor monetário.

Igualdade por valor

A igualdade por valor significa que dois objetos com os mesmos atributos são considerados iguais,
e essa semântica deve estar implementada em equals e hashCode, ou no equivalente da linguagem. O
efeito aparece em lugares que o time nem sempre espera: conjuntos que eliminam endereços duplicados
de entrega, dicionários que indexam cupons por código sem dupla entrada, e comparações de snapshot
que detectam mudança real entre duas versões de um objeto. A implementação deve incluir todos os
atributos que compõem o significado do valor e excluir qualquer campo técnico, como cache interno.
Em linguagens modernas a plataforma ajuda: records em Java geram equals, hashCode e toString a
partir dos componentes declarados, e structs em C# oferecem semântica de valor nativa. Ainda assim,
vale testar explicitamente que a igualdade considera o que deve considerar, porque um atributo a
mais quebra a equivalência.

Validação no construtor

O objeto de valor é o lugar natural para centralizar a regra do que é um dado válido. O construtor
valida tudo e falha de imediato, de modo que existir uma instância é a prova de que o valor é
legítimo. Esse desenho move a validação da periferia, onde ela costuma ser repetida em cada endpoint,
para o núcleo, onde acontece uma única vez. O exemplo a seguir, em Java moderno com records, mostra
três objetos de valor clássicos com suas regras internas.

// Dinheiro: impede moedas misturadas e centraliza aritmética monetária
public record Dinheiro(BigDecimal montante, Moeda moeda) {
    public Dinheiro {
        if (montante == null || montante.scale() > 2) throw new MontanteInvalido(montante);
        if (moeda == null) throw new MoedaObrigatoria();
    }
    public Dinheiro adicionar(Dinheiro outro) {
        if (!moeda.equals(outro.moeda)) throw new MoedaIncompativel(moeda, outro.moeda);
        return new Dinheiro(montante.add(outro.montante), moeda);  // nova instância, sem mutação
    }
    public boolean cobre(Dinheiro outro) {
        return montante.compareTo(outro.montante) >= 0 && moeda.equals(outro.moeda);
    }
}

// Cpf: regra de formato e dígitos verificadores mora no próprio tipo
public record Cpf(String numero) {
    public Cpf {
        String digitos = numero == null ? "" : numero.replaceAll("\\D", "");
        if (digitos.length() != 11 || !verificadoresValidos(digitos)) throw new CpfInvalido(numero);
        numero = digitos;                     // normaliza no momento da criação
    }
    private static boolean verificadoresValidos(String d) {
        int s1 = 0, s2 = 0;
        for (int i = 0; i < 9; i++) { int v = d.charAt(i) - '0'; s1 += v * (10 - i); s2 += v * (11 - i); }
        int dv1 = (s1 * 10) % 11 % 10;        // primeiro dígito verificador
        s2 += dv1 * 2;
        int dv2 = (s2 * 10) % 11 % 10;        // segundo dígito verificador
        return dv1 == d.charAt(9) - '0' && dv2 == d.charAt(10) - '0';
    }
}

// Email: valida formato uma única vez para todo o sistema
public record Email(String endereco) {
    private static final Pattern PADRAO = Pattern.compile("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$");
    public Email {
        if (endereco == null || !PADRAO.matcher(endereco).matches()) throw new EmailInvalido(endereco);
        endereco = endereco.toLowerCase();    // evita dois cadastros com caixas diferentes
    }
}

Depois desses tipos, nenhum endpoint, worker ou job em lote precisa revalidar CPF ou moeda: receber
o objeto é receber a garantia.

Exemplos clássicos e o que revelam

Dinheiro é o exemplo mais instrutivo porque uma variável do tipo BigDecimal com nome de "valor"
esconde três decisões: precisão, arredondamento e moeda. Erros de moeda misturada não aparecem em
teste unitário com valores iguais, e sim na conciliação de fim de mês entre sistemas. CPF com
verificação de dígitos elimina a classe de bugs em que o dado passa pela API e morre no órgão
receptor. Email normalizado em minúsculas evita cadastro duplicado do mesmo cliente. Um objeto de
valor pode também representar conceito não físico, como intervalo de datas, faixa de score ou
política de parcelamento, sempre com a mesma tríade de imutabilidade, igualdade por valor e
validação na criação.

Quando o objeto de valor não se paga

Há situações em que criar um tipo para cada conceito vira cerimônia sem retorno, e reconhecê-las
ajuda a manter o padrão com credibilidade. Campos que não têm regra alguma e não participam de
assinatura sensível podem permanecer primitivos, como um campo de observação livre. Prototipagem
descartável também não merece a modelagem, porque o aprendizado vem antes da estabilidade. O
critério de adoção é objetivo: se existe regra de validação, operação própria ou risco de confusão
entre tipos, o objeto de valor se paga no primeiro bug evitado; se o conceito é apenas um dado
sem comportamento, um primitivo bem nomeado é suficiente. A decisão também é reversível, e
introduzir o tipo depois da primeira regra é um refactor barato em código bem isolado.

Value Object versus Entidade e versus DTO

Critério Objeto de valor Entidade DTO
Definição Atributos, sem identidade Identidade contínua Transporte de dados entre camadas
Igualdade Por valores Por identificador Irrelevante ao propósito
Mutabilidade Imutável Estado muda no ciclo de vida Descartável após a transferência
Validação Invariante no construtor Invariante nas transições Formato básico na borda
Vive onde Núcleo do domínio Núcleo do domínio Borda da aplicação
Exemplo Dinheiro, Cpf, Email Conta, pedido, contrato Payload de requisição HTTP

Erros comuns e boas práticas

  • Usar primitivos crus no domínio, como String para CPF e BigDecimal para dinheiro. A assinatura
    sacar(BigDecimal) aceita valor em qualquer moeda, e o erro só aparece na conciliação.
  • Criar objetos de valor com setters ou campos públicos. Qualquer mutação anula a imutabilidade e
    reintroduz a classe de defeitos que o padrão elimina.
  • Colocar comportamento que depende de identidade em um objeto de valor. Se a operação precisa
    saber qual instância específica está sendo alterada ao longo do tempo, o conceito é entidade.
  • Validar em camada de apresentação e confiar no resto do fluxo. A borda valida formato para dar boa
    mensagem ao usuário, mas a regra canônica vive no construtor do tipo.
  • Esquecer a normalização, como remover máscara de documento ou padronizar caixa de e-mail. Sem
    normalização, dois objetos com o mesmo conteúdo tornam-se diferentes e quebram a igualdade.
  • Persistir sem mapeamento dedicado. Em JPA, use embeddables ou conversores, e evite gravar os
    componentes do valor espalhados como colunas soltas de tabelas diferentes.