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


José Carlos Macoratti