Duas notas de dez idênticas
Se eu te empresto uma nota de dez e você me devolve outra nota de dez, ninguém reclama — mesmo não sendo 'a mesma' nota. Dez reais é dez reais; o valor é o que importa, não qual cédula específica. Mas se eu te empresto meu carro, você não pode devolver 'outro carro igual' — o carro tem identidade. Value Object é a nota de dez: definido pelo valor, intercambiável, sem 'qual'. Entidade é o carro: tem um id que o distingue de todos os outros iguais.
Valor × identidade
A pergunta que separa um Value Object de uma entidade é: dois deles com os mesmos campos são a mesma coisa? Se sim, é Value Object (dois 'dez reais' são iguais). Se não — se cada um tem um id que importa mesmo com campos idênticos —, é entidade (dois usuários chamados 'João Silva' são pessoas diferentes).
Dessa distinção vêm as três propriedades: igualdade por valor (comparo os campos, não a referência), imutabilidade (mudar um valor é criar outro, como somar não altera o 3) e sem identidade (não há id, não há ciclo de vida próprio).
A imutabilidade não é purismo — é o que torna o Value Object seguro de compartilhar. Se Dinheiro.somar alterasse o objeto no lugar, dois pontos do código que apontam para o mesmo valor sofreriam efeitos colaterais um do outro (o clássico bug de 'alguém mudou meu objeto por baixo'). Como somar devolve um objeto novo, o original nunca muda, e o mesmo valor pode ser passado adiante sem medo. É a mesma segurança dos tipos imutáveis de uma linguagem funcional.
O ganho maior é o domínio ficar impossível de usar errado. Com dinheiro como number, precoBRL + precoUSD compila e produz um número sem sentido; com um Value Object Dinheiro, somar checa a moeda e recusa. Com CEP como string, qualquer texto passa; com um tipo Cep, um valor inválido não chega a ser criado. Isso conversa direto com o agregado: os Value Objects são os blocos de construção de dentro dele — a raiz guarda invariantes entre objetos, e os Value Objects guardam as regras de cada valor.
Como isso aparece na prática
O bug de centavos que sumiu
Cenário
Valores monetários trafegavam como number em ponto flutuante. Arredondamentos inconsistentes espalhados pelo código faziam totais fecharem com um centavo de diferença, e a conciliação financeira vivia acusando divergências.
Como o padrão entra
Um Value Object Dinheiro passou a guardar o valor em centavos inteiros e a concentrar o arredondamento num lugar só, na criação. Toda a aritmética passou por ele.
Trade-off: Todo valor monetário passou a ser embrulhado e desembrulhado num tipo, em vez de um number direto. Em troca, o centavo fujão desapareceu, porque a regra de arredondamento deixou de estar em dezenas de lugares.
A soma de moedas diferentes que virava erro
Cenário
Um relatório somava valores de pedidos internacionais tratando tudo como number. Ocasionalmente, valores em moedas diferentes eram somados sem conversão, produzindo totais silenciosamente errados.
Como o padrão entra
Com Dinheiro carregando a moeda, somar valores de moedas diferentes passou a lançar um erro explícito, forçando uma conversão consciente antes da soma.
Trade-off: O código que antes 'só somava' passou a precisar tratar conversão de moeda de propósito. É exatamente o objetivo: o que era um erro silencioso virou um erro impossível de ignorar.
Onde isto dá errado
Deixar o Value Object mutável
Se dá para alterar um Value Object no lugar, dois pontos que compartilham a mesma instância se afetam sem querer. A imutabilidade é o que torna seguro passá-lo adiante: mudar deve criar um novo, nunca alterar o existente.
Igualdade por referência em vez de por valor
Se dois 'dez reais' criados separadamente não são considerados iguais, o tipo não é um Value Object de verdade. A igualdade tem que comparar o conteúdo — é isso que define o conceito.
Transformar tudo em Value Object
Nem todo primitivo merece um tipo. Um campo solto e sem regra envolvido num Value Object só adiciona cerimônia. O ganho aparece quando há validação, unidade ou invariante para proteger — não pela estética.
Do number cru ao tipo com regras
// PRIMITIVE OBSESSION: dinheiro como number cru espalha regras e bugs.
function total(preco: number, moeda: string) { /* ... */ }
total(10.005, "BRL"); // arredondamento? centavos? e se somar USD + BRL?
// VALUE OBJECT: um tipo definido pelo VALOR, imutavel, com igualdade
// por conteudo e as regras do conceito la dentro.
class Dinheiro {
private constructor(readonly centavos: number, readonly moeda: string) {}
static de(reais: number, moeda: string) {
return new Dinheiro(Math.round(reais * 100), moeda); // regra num lugar so
}
somar(outro: Dinheiro): Dinheiro {
if (outro.moeda !== this.moeda) throw new Error("moedas diferentes");
return new Dinheiro(this.centavos + outro.centavos, this.moeda); // NOVO objeto
}
igual(outro: Dinheiro) { // igualdade por VALOR, sem id
return this.centavos === outro.centavos && this.moeda === outro.moeda;
}
}
// Dinheiro.de(10, "BRL") e SEMPRE igual a Dinheiro.de(10, "BRL").
// Nao existe "este" dez reais e "aquele": dez reais e dez reais.Quando usar × quando evitar
Use quando
3- Quando um primitivo carrega regras que vivem espalhadas.
- Para conceitos definidos pelo valor: dinheiro, datas, medidas.
- Ao ver a mesma validação de um campo repetida em vários lugares.
Evite quando
2- Para dados soltos e sem regra, onde o primitivo basta.
- Quando o conceito precisa de identidade e ciclo de vida (é entidade).
Testar o que ficou
3 armadilhas de Value Object viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
record do Java / C#Tipos imutáveis definidos pelos valores, com igualdade por conteúdo — a linguagem dando suporte de primeira classe ao Value Object.Money, CPF, Email como tipoEnvelopar um valor num tipo próprio, em vez de usar number ou string cru, é criar um Value Object com as regras do conceito dentro.igualdade por valorDois objetos são iguais se seus campos são iguais, sem id — é o que distingue um Value Object de uma entidade.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Igualdade por valor; imutável.
const a = Money.of(10, "BRL");
a.equals(Money.of(10, "BRL")); // trueO que ele cobra
Nenhum salto a mais entre a chamada e o efeito.
- Cada valor vira um tipo a criar e manter, em vez de um primitivo usado direto
- A imutabilidade obriga a criar um novo objeto a cada mudança, em vez de alterar no lugar
Não vale se: o valor é um dado solto sem regras nem invariantes — envolver um número puro num tipo próprio só adiciona cerimônia sem proteger nada.
Onde isto aparece
Continue por aqui
- AgregadoUm agregado é um grupo de objetos que muda junto e precisa ser consistente junto, tratado como uma única unidade por trás de uma raiz. Só a raiz é acessada de fora; ela guarda as invariantes que valem para o conjunto. É a fronteira que responde à pergunta mais difícil da modelagem: o que precisa estar consistente no mesmo instante, e o que pode esperar.
- SRP — Responsabilidade ÚnicaUm módulo deve ter apenas uma razão para mudar — ou seja, deve responder a um único interessado do negócio, e não acumular decisões que evoluem por motivos diferentes.
- Máquina de estadosDeclare os estados possíveis e as transições permitidas numa tabela explícita: tudo que não está nela é impossível por construção. É a diferença entre 'status é uma string que a gente atualiza' e um ciclo de vida com regras.