.NET
- Criando uma classe abstrata para o identificador
![]() |
Criar uma classe abstrata para compartilhar o identificador em Entidades é uma boa prática mas deve ser feito com cuidado. |
A prática de criar uma classe abstrata com um identificador que será herdado pelas entidades do domínio apresenta algumas vantagens.
Dentre as quais destacamos:
DRY (Don't Repeat Yourself): Evita repetir public Guid
Id { get; private set; } em todas as entidades.
Consistência:
Garante que todas as entidades no seu modelo usem o mesmo tipo de identificador
(Guid, long, etc.).
Polimorfismo: Permite criar métodos
ou repositórios genéricos que operam sobre qualquer Entity.
Implementação de Igualdade: É o lugar perfeito para implementar a
sobrecarga dos operadores ==, != e dos métodos Equals()
e GetHashCode() baseados no Id, que é a definição de igualdade para uma
entidade.
Abaixo temos um código básico que pode ser usado para fazer esta implementação:
public abstract class Entity { public Guid Id { get; protected set; } protected Entity() { Id = Guid.NewGuid(); } public override bool Equals(object? obj) { if (obj is not Entity other) return false; if (ReferenceEquals(this, other)) return true; return Id == other.Id; } public static bool operator ==(Entity a, Entity b) => a.Equals(b); public static bool operator !=(Entity a, Entity b) => !(a == b); public override int GetHashCode() => Id.GetHashCode(); } |
A seguir como podemos usar esta classe:
public class Pedido : Entity { // A propriedade 'Id' não precisa mais ser declarada aqui! public Guid ClienteId { get; private set; } // ... resto da classe } |
Aqui alguém poderia questionar o seguinte:
Porque estamos definindo o método Equals na implementação se a classe abstrata vai ser usada apenas em Entidades, visto que a comparação baseada em atributos é a marca registrada dos Value Objects ?
A resposta é: Nós redefinimos o conceito de igualdade para uma Entidade para que ele corresponda à sua definição no DDD.
Para entender vamos lembrar alguns conceitos do DDD:
Igualdade de
Value Object: Dois VOs são iguais se todos os seus atributos forem
iguais. (Ex: new Dinheiro(10, "BRL") é igual a new
Dinheiro(10, "BRL")).
Igualdade de Entidade:
Duas Entidades são iguais se elas tiverem a mesma identidade (ID), mesmo que
todos os seus outros atributos sejam diferentes.
Por que o Comportamento Padrão do C# é Perigoso para Entidades?
Vamos analisar o que aconteceria se não implementássemos esse código. Por
padrão, o C# (e muitas outras linguagens) compara objetos de referência (class)
de duas maneiras:
== (Operador de Igualdade): Compara se
as duas variáveis apontam para o mesmo objeto na memória (igualdade de
referência).
.Equals() (Método): Por padrão, para
classes, ele também faz uma verificação de igualdade de referência.
Cenário do Problema (Sem a Sobrescrita):
Imagine que
você tem um repositório que busca um pedido do banco de dados.
// 1. Você busca um pedido var pedido1 = pedidoRepository.GetById(pedidoId); // Objeto A na memória // 2. Em outra parte do código, talvez em outra requisição, você busca o MESMO pedido var pedido2 = pedidoRepository.GetById(pedidoId); // Objeto B na memória // Agora, vamos comparar Console.WriteLine(pedido1.Id == pedido2.Id); // Imprime: true (os IDs são iguais) Console.WriteLine(pedido1 == pedido2); // Imprime: false (são objetos diferentes na memória!) Console.WriteLine(pedido1.Equals(pedido2)); // Imprime: false (padrão também checa referência) |
Este é um desastre para a lógica de negócio!
Do ponto de vista do seu domínio, pedido1 e pedido2 são a mesma entidade. Eles representam o mesmo pedido de compra no mundo real. Mas para a linguagem, eles são diferentes.
Isso pode causar bugs sutis e terríveis:
- Se você tiver uma lista
de pedidos e verificar se ela Contains(pedido2), o resultado
pode ser false mesmo que um objeto representando o mesmo pedido
já esteja lá.
- Em testes unitários, comparar um objeto que você criou com o que foi retornado de um mock do repositório falharia.
- Lógicas que dependem de comparação de entidades se tornariam imprevisíveis.
Ao colocar aquele código na classe base Entity, nós ensinamos ao C# a regra de negócio do DDD para a igualdade de entidades.
Vamos analisar linha por linha:
public override bool Equals(object? obj) { // 1. Se o outro objeto não é uma Entidade (ou é nulo), eles não podem ser iguais. if (obj is not Entity other) return false; // 2. Se as variáveis apontam para o mesmíssimo objeto na memória, eles são iguais. // É uma otimização para evitar a próxima verificação. if (ReferenceEquals(this, other)) return true; // 3. AQUI ESTÁ A DEFINIÇÃO DO DDD! // Duas entidades são consideradas iguais se, e somente se, seus IDs forem iguais. return Id == other.Id; } |
E as outras partes:
// Garante que o operador '==' use a nossa nova lógica de 'Equals' // em vez do padrão de comparação de referência. public static bool operator ==(Entity a, Entity b) => a.Equals(b); public static bool operator !=(Entity a, Entity b) => !(a == b); // Se você sobrescreve 'Equals', você DEVE sobrescrever 'GetHashCode'. // A regra é: se dois objetos são 'Equals', eles DEVEM ter o mesmo 'GetHashCode'. // Como nossa igualdade depende apenas do Id, nosso GetHashCode também deve depender apenas do Id. // Isso é crucial para o funcionamento correto de dicionários e hash sets. public override int GetHashCode() => Id.GetHashCode(); |
Concluindo:
Uma entidade é definida por sua identidade. Isso significa que, para o nosso
negócio, dois objetos de Pedido são o mesmo pedido se eles
tiverem o mesmo Id.
No entanto, por padrão, a linguagem de programação
não sabe disso. Ela acha que dois objetos só são iguais se estiverem no mesmo
lugar na memória.
Isso é um problema. Precisamos ensinar à linguagem a
nossa regra de negócio.
Para fazer isso, adicionamos o código à nossa
classe base Entity. O que este código faz é simples: ele diz
que sempre que você comparar duas entidades, a única coisa que importa é o Id.
Se os Ids forem iguais, as entidades são iguais.
E estamos conversados...
"A ti clamarei, ó Senhor, Rocha minha;
não emudeças para comigo; não aconteça, calando-te tu para comigo, que eu fique
semelhante aos que descem ao abismo"
Salmos 28:1
Referências:
NET - Unit of Work - Padrão Unidade de ...