EF Core  -  Evitando o modelo anêmico


  Hoje veremos alguns procedimentos que podemos usar para evitar a criação de um modelo anêmico com o EF Core.

Quando usamos o EF Core em nossos projetos é muito comum sermos direcionados para o uso de modelo anêmico.

Um modelo de domínio anêmico é um modelo sem comportamentos onde temos diversas propriedades com get e set definidas sem lógica alguma e onde o cliente(quem vai usar a classe) da classe tem controle sobre como instanciar e modificar a classe.

Nota: Os modelos de domínio anêmicos são frequentemente descritos como um antipadrão devido a uma completa falta de princípios OO.

Nesses modelos, o cliente precisa interpretar o objetivo e o uso da classe e a lógica é enviada para outras classes, denominadas serviços da classe de domínio. Com a lógica em outra classe, não há nada que ajude o cliente a navegar ou usar a classe de modelo.

Essa abordagem acarreta muitos problemas e desvantagens de design como :

No entanto podemos evitar o modelo anêmico com alguns procedimentos e assim termos um modelo de domínio rico que inclui nas classes de domínio os dados e o comportamento e onde a lógica de domínio faz parte das entidades.

Para ilustrar como podemos mover um modelo anêmico para um modelo mais rico vamos partir de uma classe Produto que representa um modelo de domínio e que possui o seguinte código:

public class Produto
{
    public int Id { get; set; }

    [Required]
    [StringLength(250)]

    public string? Nome { get; set; }

    [Required]
    [StringLength(500)]

    public string? Descricao { get; set; }

    [Required]
    public decimal Preco { get; set; }

    public DateTime DataAquisicao { get; set; }

    public ProdutoStatus Status { get; set; }
}

Este é um exemplo típico de uma classe que representa um modelo anêmico usada na abordagem Code-First do EF Core. Ela mais se assemelha a um DTO-Data Transfer Object visto que dependem de uma implementação para validação e definição de uma lógica de negócios.

No entanto podemos melhorar este modelo anêmico enriquecendo-o com alguns procedimentos bem simples que iremos ver a seguir.

1- Removendo os construtores públicos sem parâmetros

Quando não especificamos um construtor em uma classe ela terá um construtor público sem parâmetros e assim qualquer um poderá criar uma instância desta classe usando uma declaração com o operador new:

var produto = new Produto();

Com isso é grande a chance de termos um objeto criado em um estado inválido.

Ocorre que objetos de domínio normalmente requerem pelo menos alguns dados para torná-los válidos. Criar uma instância de Produto sem quaisquer dados, como Nome, Descrição, Preço não tem sentido.

Sem alguns dados úteis para identificação, não faz sentido permitir tal instância, e, para ajudar com isso, podemos tratar nossa classe de domínio como qualquer outra classe que segue o paradigma da orientação a objetos e introduzir um construtor parametrizado:

public class Produto
{
    public Produto(string? nome, string? descricao, decimal preco,
        DateTime dataAquisicao, ProdutoStatus status)
    {
        Nome = nome;
        Descricao = descricao;
        Preco = preco;
        DataAquisicao = dataAquisicao;
        Status = status;
    }

    [Required]
    [StringLength(250)]

    public string? Nome { get; set; }

    [Required]
    [StringLength(500)]

    public string? Descricao { get; set; }

    [Required]
    public decimal Preco { get; set; }

    public DateTime DataAquisicao { get; set; }
    public ProdutoStatus Status { get; set; }
}

Com esta pequena alteração, agora para poder criar um objeto do domínio será necessário fornecer um mínimo de dados para satisfazer o construtor e com isso estamos garantindo que teremos um objeto em um estado válido e como consequência não precisamos verificar valores inválidos pois na instanciação do objeto já estamos validando o objeto.

Além disso, agora qualquer código de chamada sabe exatamente o que é necessário para instanciar o objeto. Com um construtor sem parâmetros, isso é desconhecido por quem vai usar a classe e é muito fácil construir um objeto com dados ausentes.

Acontece que se você estiver usando o EF Core, após fazer essa alteração vai receber a seguinte mensagem de erro ao tentar recuperar entidades do banco de dados:

InvalidOperationException: A parameterless constructor was not found on entity type 'Produto'. In order to create an instance of 'Produto' EF requires that a parameterless constructor be declared.

Isso ocorre porque o EF Core exige o construtor sem parâmetros mas ele não precisa ser público. Assim podemos criar um construtor sem parâmetros interno ou privado na classe para atender a exigência do EF Core:

public class Produto
{

   
private Produto()
    {}

    public Produto(string? nome, string? descricao, decimal preco,
        DateTime dataAquisicao, ProdutoStatus status)
    {
        Nome = nome;
        Descricao = descricao;
        Preco = preco;
        DataAquisicao = dataAquisicao;
        Status = status;
    }

    [Required]
    [StringLength(250)]

    public string? Nome { get; set; }

    [Required]
    [StringLength(500)]

    public string? Descricao { get; set; }

    [Required]
    public decimal Preco { get; set; }

    public DateTime DataAquisicao { get; set; }
    public ProdutoStatus Status { get; set; }
}

Ter o construtor adicional obviamente não é o ideal, mas esse tipo de compromisso geralmente é necessário para que os ORMs funcionem bem com o código orientado a objetos.

Nesse momento, o método de fábrica gerado funcionará para criar instâncias para usuários, mas o sistema usará o construtor privado sem parâmetros para materializar objetos que são resultados de consultas.

2- Removendo os setters das propriedades públicas

Com a introdução do construtor parametrizado garantimos que quando o objeto for instanciado ele esteja em um estado válido, entretanto isso não impede que os valores das propriedades sejam alterados para valores inválidos posteriormente.

Para evitar este tipo de problema podemos :

a- Adicionar lógica de validação aos setters de propriedade;
b- Impedir a modificação direta de propriedades e, em vez disso, usar métodos correspondentes às ações do usuário;


Adicionar validação ao setter de propriedade é perfeitamente aceitável, mas significa que não podemos mais usar o recurso das Propriedades Automáticas e devemos introduzir um campo de apoio.

Para exemplificar vamos adicionar uma lógica de validação à propriedade Nome:

[Required]
[StringLength(250)]

private string nome;

public string Nome
{
    get { return nome; }
    set
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            throw new ArgumentException("O nome deve conter um valor");
         }
         nome = value;
    }

}

Embora usar esta opção resolva o problema a segunda opção é mais adequada pois ela modela mais de perto o que acontece no mundo real. Em vez de atualizar uma única propriedade isoladamente, os usuários tendem a realizar um conjunto de ações conhecidas (determinadas pela interface do usuário ou da API).

Essas ações podem resultar na atualização de uma ou mais propriedades, mas geralmente há mais do que isso. É muito comum ter cenários em que a lógica de negócios depende do contexto, o que pode tornar a lógica de validação do definidor de propriedades complexa e difícil de entender.

Como exemplo básico, considere o seguinte processo de aquisição de um produto:

public void AdquirirProduto()
{
    if (Status == ProdutoStatus.EstoqueMinimo || Status == ProdutoStatus.ForaEstoque)
    {
        DataAquisicao = DateTime.UtcNow;
        Status = ProdutoStatus.EmEstoque;
    }
}

Temos aqui um método para aquisição de um produto com uma lógica bem simples onde temos duas propriedades que podem ser atualizadas: DataAquisicao e Status.

Ao invés de definir essa lógica em um método como mostrado acima poderíamos implementar isso usando um setter de propriedades, entretanto o código não ficaria tão claro e transparente como fizemos usando o método. Isso pode ser percebido ao fazer a chamada ao método a partir de outra classe:

O que encontramos neste cenário não é validação no objeto de domínio mas a definição da lógica de validação sendo feita em uma camada distinta o que pode resultar em:

Ainda temos que considerar que o EF Core não vai funcionar corretamente se removermos completamente o setter de todas as propriedades, mas podemos alterar o nível de acesso para privado e resolver o problema :

public class Produto
{
    ...
    [Required]
    [StringLength(500)]
    public string? Descricao { get; private set; }

    [Required]
    public decimal Preco { get; private set; }

    public DateTime DataAquisicao { get; private set; }

    public ProdutoStatus Status { get; private set; }
     ...
}

Agora todas as propriedades são somente leitura fora da classe. Para permitir atualizações em nossas classes de domínio, podemos criar métodos,  como o método AdquirirProduto() mostrado acima.

Ao remover o construtor sem parâmetros e os setters de propriedades públicas e adicionar métodos do tipo ação, agora temos objetos de domínio que são sempre válidos e contêm toda a lógica de negócios diretamente relacionada às entidades em questão.

Esta é uma grande melhoria. Tornamos nosso código mais robusto e simples ao mesmo tempo. Todo o código que adicionamos aos objetos de domínio pode ser removido do nível superior da pilha de chamadas, onde muitas vezes é duplicado em vários lugares diferentes.

Embora possamos discutir outros conceitos de DDD, como eventos de domínio e o uso de serviços de domínio por meio do padrão de despacho duplo, suas vantagens, principalmente no que diz respeito à simplicidade, são muito menos claras. Um conceito DDD que geralmente simplifica seu código é o uso de objetos de valor que discutiremos a seguir.

2- Introduzindo objetos de valor ou Value Objects

Um Value Object é um tipo imutável que é distinguível apenas pelo estado de suas propriedades, ou seja, ele não possui uma identidade, e, de forma simples e óbvia, ele é um objeto que representa um valor.

Os objetos de valor geralmente podem ser usados para substituir uma ou mais propriedades em um objeto de domínio.        

Exemplos clássicos de objetos de valor incluem dinheiro, endereços e coordenadas, mas também pode ser benéfico substituir uma única propriedade por um tipo de valor em vez de usar uma string ou int.

Por exemplo, em vez de armazenar um número de telefone como uma string, você pode criar um tipo de valor NumeroTelefone com validação integrada, bem como métodos para extrair o código de discagem etc.

Como exemplo temos a seguir um código que mostra um Value Object Dinheiro implementado como uma classe para ser usada no EF Core:

public class Dinheiro
{
    [StringLength(3)]
    public string? Moeda { get; private set; }

    public int Valor { get; private set; }

    private Dinheiro()
    {}

    public Dinheiro(string moeda, int valor)
    {
        // validação
        Moeda = moeda;
        Valor = valor;
    }
}

A Moeda e o Valor estão ligados de forma intrínseca e ambas as informações são necessárias para que os dados sejam úteis por isso é válido definir este modelo.

Observe o uso de um construtor parametrizado, dos setters de propriedade definidos como private e do construtor privado sem parâmetros usado para atender ao EF Core.

No contexto de persistência de dados (RDBMS), um tipo de valor não reside em uma tabela de banco de dados separada. Para nos permitir usar objetos de valor no Entity Framework, é necessário um pequeno ajuste que vai  depender da versão do EF Core que você está usando.

No EF Core 6, podemos usar o atributo [Owned] para identificar um Value Object:

[Owned]
public class Dinheiro
{
    ...
}

Com isso agora podemos usar o objeto de valor Dinheiro em nossa entidade Produto da seguinte maneira:

public class Produto
{
    ...  

   [Required]
   public Dinheiro Preco { get; private set; }

     ...
}

Como consequência após aplicar o Migrations devemos ter na tabela do banco de dados  a criação de duas colunas adicionais:


 Preco_Moeda      
 Preco_Valor

 

Dessa forma, um Value Object  se valida para que o modelo de domínio que hospeda a sua propriedade não precise saber como validar o tipo de valor e pode ser simplificado.

Quando você passar de um modelo de domínio anêmico para um modelo mais rico,  vai apreciar os benefícios de encapsular a lógica de negócios de nível de domínio em seus objetos de domínio.

É importante estar ciente de que ter um modelo de domínio rico não nega o requisito de criar outra camada para orquestrar essas preocupações de nível superior.

E estamos conversados...

'Sede pois, irmãos, pacientes até à vinda do Senhor. Eis que o lavrador espera o precioso fruto da terra, aguardando-o com paciência, até que receba a chuva temporã e serôdia.'
Tiago 5:7

Referências:


José Carlos Macoratti