ArquiteturaIntermediário6 min de leitura5 seções2003, Data aproximada

Value Object

Nem tudo no domínio tem identidade. Dez reais é dez reais; um CEP é um CEP — não existe 'este' e 'aquele', existe o valor. Um Value Object é um tipo definido só pelo seu conteúdo, imutável e com igualdade por valor, que carrega as regras daquele conceito em vez de deixá-las espalhadas sobre um number ou string cru. É o antídoto da obsessão por primitivos.

Em 10 segundos

Um Value Object é um tipo definido pelo seu conteúdo — imutável, com igualdade por valor — que carrega as regras de um conceito em vez de deixá-las soltas sobre um number ou string. Dez reais é sempre igual a dez reais; não há 'este' e 'aquele'. Ele mata a obsessão por primitivos: a validação e a unidade vivem no tipo, uma vez, e o domínio passa a impedir o que não faz sentido — como somar moedas diferentes.

Antes de ter nome

A distinção entre coisas com identidade e coisas definidas só pelo valor (um número, uma cor) é anterior — tipos imutáveis existem desde a matemática.

  • #ddd
  • #dominio
  • #imutabilidade
  • #tipo
  • #igualdade
Analogia

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.

01Conceito

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.

02No mundo real

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.

03Cuidado

Onde isto dá errado

  1. 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.

  2. 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.

  3. 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.

04Implementação

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.
05Decisão

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")); // true

O 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