equals e repositórios recebendo VOConforme Value Objects entram nos domínios (Slug, Username, Phone,
Email), surge a pergunta recorrente de como comparar um VO: contra outro VO
ou contra o primitivo embrulhado.
O sintoma concreto foi o bug do findBySlug, que comparava
s.props.slug === slug — um objeto Slug contra uma string — sempre dando
false. É uma classe de bug que reaparece a cada novo VO e a cada novo
findBy*, porque a comparação estava ad-hoc: uns lugares desembrulhavam com
.value, outros comparavam referência de objeto, e as assinaturas dos
repositórios recebiam string solta sem deixar claro o que esperar.
Vale para comparação de Value Objects e para o contrato de busca dos
repositórios por um VO. Complementa o ADR
0004 (R3): lá o .value
de escrita foi centralizado no mapper; aqui o .value de leitura/comparação é
disciplinado pela mesma lógica — um VO se compara como VO.
1. Todo VO expõe equals(other: T): boolean. Compara por valor, não por
referência. Slug, Username e Phone têm; novos VOs (Email, etc.) devem
seguir o mesmo contrato.
2. Repositórios recebem o VO, não o primitivo. As assinaturas de busca por um campo que é VO recebem o tipo do VO:
StreamerRepository.findBySlug(slug: Slug)ProfileRepository.findByUsername(username: Username)ProfileRepository.findByPhone(phone: Phone)Isso torna a intenção explícita no call site e impede a comparação acidental objeto-vs-string.
3. A comparação acontece na fronteira certa de cada adapter:
.equals():
this.items.find(s => s.props.slug.equals(slug)).WHERE, num lugar só:
eq(table.slug, slug.value). O .equals() não roda no banco; o que a regra
garante é que o repo recebe o VO e o .value aparece só ali.4. Use cases constroem o VO uma vez e o repassam. Em vez de espalhar
vo.value pelos use cases, cria-se o VO e ele atravessa a comparação, o
findBy* e a criação do agregado. (Efeito colateral positivo: corrigiu o
onboard-streamer, que checava existência com a slug crua mas gravava a
normalizada — agora usa a mesma Slug nos dois.)
findBySlug(slug: Slug))..value concentrado: na escrita, no mapper (ADR 0004); na leitura, no
WHERE do adapter SQL..equals()), espelhando o
adapter real em vez de mascarar diferenças.findBy* por campos que não são VO (ex.: ids UniqueId, datas)
continuam com o primitivo; a regra é só para campos modelados como VO.Email (ou outro VO) num findBy* → seguir o mesmo contrato
(equals + assinatura recebendo o VO).Slug de create vs createFromText) → o equals é o lugar de
decidir isso; documentar em ADR próprio se a regra de igualdade ficar não-trivial.