Value Objects
Definição
Seção intitulada “Definição”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.
Características essenciais
Seção intitulada “Características essenciais”- Imutável — nunca muda após criado; operações retornam novas instâncias.
- Igualdade estrutural —
equals/hashCodesobre todos os atributos (automático emrecord). - Sem identidade — não tem ID nem ciclo de vida próprio.
- Autovalidante — garante suas invariantes na criação (um
CPFinválido não existe). - Side-effect free — métodos não alteram estado; são funções puras.
O problema resolvido
Seção intitulada “O problema resolvido”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.
Exemplo em Java + Spring
Seção intitulada “Exemplo em Java + Spring”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
}
Anti-padrões e erros comuns
Seção intitulada “Anti-padrões e erros comuns”- Primitive Obsession: usar
String,BigDecimal,intcrus 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.)
Relações
Seção intitulada “Relações”- Contrasta com Entidades
- Compõe Entidades e Agregados (inclusive como identificadores tipados)
- Materializa a Linguagem Ubíqua em tipos pequenos
- Bloco de Design Estratégico e Tático e parte de Domain-Driven Design
- Voltar para Home
Perguntas de revisão
Seção intitulada “Perguntas de revisão”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.