Pular para o conteúdo

Value Objects

Para Eric Evans, um Value Object descreve uma característica de algo. Não importa qual instância você tem, apenas o valor. Uma nota de R$ 10 é intercambiável com outra nota de R$ 10 — não há identidade que as distinga.

Vaughn Vernon recomenda modelar o máximo possível como Value Objects. Eles são mais simples, seguros e testáveis que Entidades, e empurram invariantes para tipos pequenos e coesos.

  1. Imutável — nunca muda após criado; operações retornam novas instâncias.
  2. Igualdade estruturalequals/hashCode sobre todos os atributos (automático em record).
  3. Sem identidade — não tem ID nem ciclo de vida próprio.
  4. Autovalidante — garante suas invariantes na criação (um CPF inválido não existe).
  5. Side-effect free — métodos não alteram estado; são funções puras.

Elimina a obsessão por primitivos (Primitive Obsession). Em vez de String cpf e BigDecimal valor soltos, criam-se tipos Cpf e Dinheiro que centralizam validação e comportamento, tornando estados inválidos irrepresentáveis.

Value Objects com record: imutáveis, autovalidantes, com comportamento (funções puras).

package com.loja.shared.domain;
import java.util.regex.Pattern;
public record Cpf(String numero) {
private static final Pattern SOMENTE_DIGITOS = Pattern.compile("\\d{11}");
public Cpf {
if (numero == null || !SOMENTE_DIGITOS.matcher(numero).matches()) {
throw new IllegalArgumentException("CPF deve conter 11 digitos");
}
if (numero.chars().distinct().count() == 1) {
throw new IllegalArgumentException("CPF invalido");
}
if (!digitosVerificadoresValidos(numero)) {
throw new IllegalArgumentException("CPF invalido");
}
}
public String mascarado() {
return "***." + numero.substring(3, 6) + ".***-**";
}
private static boolean digitosVerificadoresValidos(String cpf) {
int soma1 = 0;
for (int i = 0; i < 9; i++) {
soma1 += (cpf.charAt(i) - '0') * (10 - i);
}
int dv1 = 11 - (soma1 % 11);
if (dv1 >= 10) {
dv1 = 0;
}
int soma2 = 0;
for (int i = 0; i < 10; i++) {
soma2 += (cpf.charAt(i) - '0') * (11 - i);
}
int dv2 = 11 - (soma2 % 11);
if (dv2 >= 10) {
dv2 = 0;
}
return dv1 == (cpf.charAt(9) - '0') && dv2 == (cpf.charAt(10) - '0');
}
}

A rejeição explícita de sequências com todos os dígitos iguais (111.111.111-11 etc.) não é paranoia: essas sequências passam na fórmula dos dígitos verificadores e são o falso positivo clássico dos validadores caseiros de CPF.

package com.loja.shared.domain;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Currency;
public record Dinheiro(BigDecimal valor, Currency moeda) {
public Dinheiro {
if (valor == null || moeda == null) {
throw new IllegalArgumentException("Valor e moeda obrigatorios");
}
valor = valor.setScale(2, RoundingMode.HALF_EVEN);
}
public static Dinheiro reais(String valor) {
return new Dinheiro(new BigDecimal(valor), Currency.getInstance("BRL"));
}
public Dinheiro somar(Dinheiro outro) {
garantirMesmaMoeda(outro);
return new Dinheiro(this.valor.add(outro.valor), this.moeda);
}
public Dinheiro subtrair(Dinheiro outro) {
garantirMesmaMoeda(outro);
return new Dinheiro(this.valor.subtract(outro.valor), this.moeda);
}
public Dinheiro multiplicar(int fator) {
return new Dinheiro(this.valor.multiply(BigDecimal.valueOf(fator)), this.moeda);
}
public boolean maiorQue(Dinheiro outro) {
garantirMesmaMoeda(outro);
return this.valor.compareTo(outro.valor) > 0;
}
private void garantirMesmaMoeda(Dinheiro outro) {
if (!this.moeda.equals(outro.moeda)) {
throw new IllegalArgumentException("Moedas diferentes");
}
}
}
classDiagram
    class Dinheiro {
        <<Value Object>>
        +BigDecimal valor
        +Currency moeda
        +somar(Dinheiro) Dinheiro
        +subtrair(Dinheiro) Dinheiro
        +multiplicar(int) Dinheiro
        +maiorQue(Dinheiro) boolean
    }
    class Cpf {
        <<Value Object>>
        +String numero
        +mascarado() String
    }
  • Primitive Obsession: usar String, BigDecimal, int crus onde caberia um VO com validação e semântica.
  • VO mutável: expor setters ou coleções mutáveis; quebra a igualdade estrutural e o compartilhamento seguro.
  • Igualdade por identidade: dar ID a um VO, transformando-o indevidamente em Entidade.
  • Validação fora do VO: deixar a checagem de invariantes num service, permitindo instâncias inválidas. (Validar o formato do payload na borda com Bean Validation é outra coisa, e continua existindo — ver o callout em Adapters.)

1. Por que Value Objects devem ser imutáveis?

Resposta

Para garantir igualdade estrutural estável, compartilhamento seguro entre threads/objetos e ausência de efeitos colaterais. Operações retornam novas instâncias em vez de mutar a existente.

2. Como record ajuda a implementar Value Objects em Java?

Resposta

Fornece imutabilidade, equals/hashCode estruturais e toString automaticamente. O construtor compacto (compact constructor) é o lugar ideal para validar invariantes.

3. O que é Primitive Obsession e como o VO a combate?

Resposta

É usar tipos primitivos (String, BigDecimal) para conceitos ricos de domínio. O VO encapsula validação e comportamento (Cpf, Dinheiro), tornando estados inválidos irrepresentáveis.

4. O que diferencia um VO de uma Entidade?

Resposta

O VO não tem identidade, é imutável e comparado por todos os atributos. A Entidade tem identidade (ID), ciclo de vida e é comparada por ID.