EF Core -  Como evitar o modelo de domínio anêmico ?


Hoje veremos como podemos evitar o modelo de domínio anêmico usando o EF Core.

Segundo a wikipédia, um modelo de domínio anêmico é um modelo de domínio de software em que os objetos de domínio contêm pouca ou nenhuma lógica de negócios (validações, cálculos, regras de negócios, etc.). Este padrão foi descrito por Martin Fowler que considera a prática um anti-pattern.

Modelos de domínio anêmicos são extremamente comuns ao usar ORMs como o Entity Framework, e, neste artigo veremos os problemas de ter um modelo de domínio anêmico e, em seguida, examinar algumas técnicas simples para permitir que você crie modelos mais ricos ao usar o Entity Framework Code First.

Um modelo anêmico definido ao usar o EF Core se expressa por classes que armazenam apenas dados possuindo apenas um conjunto de gets e sets sem nenhuma lógica ou validação.

Como exemplo vemos a seguir a classe BlogPost que faz parte de um  modelo de domínio usando o EF Core:

namespace EFCore_AmenicDomain.Models
{
    public enum BlogPostStatus
    {
       Publicado, Arquivado, Cancelado, Rascunho
    }
   public class BlogPost
    {
        public int Id { get; set; }
        [Required]
        [StringLength(250)]
        public string Titulo { get; set; }
        [Required]
        [StringLength(500)]
        public string Resumo { get; set; }
        [Required]
        public string Assunto { get; set; }
        public DateTime? DataPublicacao { get; set; }
        public BlogPostStatus Status { get; set; }
    }
}

Esse é o tipo mais comum de definição de modelo de domínio que você vai encontrar, pois toda a documentação, e, mesmo os exemplos encontrados, sempre demonstram a utilização do EF Core na sua forma mais simples objetivando começar o mais rápido possível o desenvolvimento.

Esse modelo anêmico é um considerado um anti-padrão devido a uma completa falta de princípios aderentes a orientação a objetos, visto que eles são objetos burros que dependem do código de chamada para validação e outras lógicas de negócios.

Como consequência, isso pode levar a repetição de código, integridade de dados ruins e maior complexidade que deve ser definida nas camadas superiores.

Movendo para um modelo de domínio mais rico

Apenas apresentar o problema é fácil, veremos a seguir 3 maneiras bem simples de enriquecer o seu modelo de domínio anêmico.

1-  Remover construtores públicos sem parâmetros

A menos que você especifique um construtor, sua classe terá um construtor sem parâmetros padrão. Isso significa que você pode instanciar sua classe da seguinte maneira:

var blogPost = new BlogPost();

Na maioria das situações isso não faz sentido, pois objetos de domínio normalmente, exigem pelo menos alguns dados para torná-los válidos.

Criar uma instância do BlogPost sem nenhum dado, como título ou assunto, não tem sentido. Sem alguns dados úteis para identificação, não há sentido em permitir tal instância.

Alguém pode discordar, mas a crença prevalecente na comunidade DDD(Domain Drive Design) é que faz sentido garantir que os objetos do domínio sejam sempre válidos.

Para ajudar com isso, podemos tratar nossa classe de domínio como qualquer outra classe e introduzir um construtor com parâmetros.

Para a nossa classe BlogPost podemos definir um construtor conforme mostra o código abaixo:

    public class BlogPost
    {
        public BlogPost(string titulo, string resumo, string assunto)
        {
            if (string.IsNullOrWhiteSpace(titulo))
            {
                throw new ArgumentException("O título é obrigatório");
            }
            //...demais validações
            Titulo = titulo;
            Resumo = resumo;
            Assunto = assunto;
            DataPublicacao = DateTime.UtcNow;
            Status = BlogPostStatus.Rascunho;
        }
        ...
        ...
}

Agora, o código de chamada deve fornecer um mínimo de dados para satisfazer o contrato (o construtor). Essa mudança fornece dois resultados positivos:

  1. Qualquer objeto BlogPost recém-instanciado agora tem a garantia de ser válido. Qualquer código agindo no BlogPost não precisa verificar valores inválidos. O objeto de domínio se valida automaticamente na instanciação.
     
  2. Qualquer código de chamada sabe exatamente o que é necessário para instanciar o objeto. Com um construtor sem parâmetros, isso é desconhecido e é muito fácil construir um objeto com dados ausentes.

Infelizmente, depois de fazer essa alteração, você descobrirá que seu código EF não funciona mais ao recuperar entidades do banco de dados, pois com certeza vai ocorrer a seguinte exceção:

InvalidOperationException: Um construtor sem parâmetros não foi encontrado no tipo de entidade 'BlogPost'. Para criar uma instância de 'BlogPost', o EF requer que um construtor sem parâmetros seja declarado.

Mas porque ?

O EF requer um construtor sem parâmetros para consulta, então o que fazer ?

Felizmente, embora o EF exija o construtor sem parâmetros, ele não precisa ser público, portanto podemos adicionar um construtor privado sem parâmetros para o EF, enquanto forçamos o código de chamada a usar o construtor parametrizado.

Ter o construtor adicional obviamente não é o ideal, mas esse tipo de comprometimento é frequentemente necessário para obter ORMs que funcionem corretamente com o código aderente a orientação a objetos.

O código então ficaria assim com o construtor privado:

    public class BlogPost
    {
        private BlogPost()
        { }

        public BlogPost(string titulo, string resumo, string assunto)
        {
            if (string.IsNullOrWhiteSpace(titulo))
            {
                throw new ArgumentException("O título é obrigatório");
            }
            //...demais validações
            Titulo = titulo;
            Resumo = resumo;
            Assunto = assunto;
            DataPublicacao = DateTime.UtcNow;
            Status = BlogPostStatus.Rascunho;
        }
        ...
        ...
}

2-  Remover propriedades setters públicas

O construtor parametrizado introduzido acima garante que, quando instanciado, o objeto esteja em um estado válido. Isso não impede que você altere os valores de propriedade para valores inválidos posteriormente. Para corrigir esse problema, temos duas opções:

  1. Adicionar lógica de validação às propriedades setters;
  2. Evitar a modificação direta de propriedades e, em vez disso, usar métodos correspondentes às ações do usuário;

Na primeira abordagem, adicionar lógica de validação ás propriedades setter é perfeitamente aceitável, mas significa que não podemos mais usar as propriedades automáticas e devemos introduzir um campo de apoio.

Como exemplo vejamos como fica a implementação para a propriedade Titulo:

...
private string titulo;

[Required]
[StringLength(250)]
public string Titulo
{
    get { return titulo; }
    set
    {
        if (string.IsNullOrWhiteSpace(value))
        {
           throw new ArgumentException("Titulo deve conter um valor");
        }
         titulo = value;
     }
}
...

No código acima criamos um campo de apoio titulo e definimos a validação do set.

Para a segunda abordagem, vamos definir um método Publicar com alguma lógica bem simples e duas propriedades que podem ser atualizadas:  Status e DataPublicacao:

public void Publicar()
{
   if (Status == BlogPostStatus.Rascunho || Status == BlogPostStatus.Arquivado)
   {
      if (Status == BlogPostStatus.Rascunho)
      {
         DataPublicacao = DateTime.UtcNow;
      }

      Status = BlogPostStatus.Publicado;
    }
}

Existe um consenso de que essa abordagem é mais robusta pois ela modela mais de perto o que ocorre no mundo real.

Em vez de atualizar uma única propriedade isoladamente, os usuários tendem a executar um conjunto de ações conhecidas (determinadas pela interface do usuário ou da API). Essas ações podem resultar em uma ou mais propriedades sendo atualizadas.

É muito comum ter cenários em que a lógica de negócios dependa do contexto, o que pode tornar a lógica de validação de propriedades setter complexa e difícil de entender.

Poderíamos também ter implementado essa lógica como uma propriedade setter , mas o código fica bem menos claro, particularmente quando o chamamos de outra classe. Compare:

1- blogPost.Status = BlogPostStatus.Publicado;

com

2- blogPost.Publicar();

Os efeitos colaterais da primeira opção não são de todo óbvios e essa falta de clareza deve sempre ser evitada.

Acontece que o que você vê na maioria dos códigos não é validação no objeto de domínio. Em vez disso, esse tipo de lógica é encontrado na próxima camada.

Isso pode acarretar as seguintes consequências:

Ocorre que o EF Core não vai funcionar corretamente se removermos completamente os set de todas as propriedades, mas mudar o nível de acesso para private resolve o problema. Exemplo:

public class BlogPost
{
   private BlogPost()
   {}

   public BlogPost(string titulo, string resumo, string assunto)
   {
       if (string.IsNullOrWhiteSpace(titulo))
       {
           throw new ArgumentException("O título é obrigatório");
       }

       //...demais validações

       Titulo = titulo;
       Resumo = resumo;
       Assunto = assunto;
       DataPublicacao = DateTime.UtcNow;
       Status = BlogPostStatus.Rascunho;
      }

      public int Id { get; private set; }    
      ...
}

Fazendo assim, agora todas as propriedades são somente leitura fora da classe.

Para permitir atualizações em nossas classes de domínio, introduzimos métodos no estilo de ação, como o método Publicar mostrado acima.

Removendo o construtor sem parâmetros e as propriedades públicas set, e, adicionando métodos de tipo de 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 mais simples ao mesmo tempo.

Vejamos agora a utilização de um conceito DDD que geralmente simplifica seu código:  o uso de objetos de valor.

3-  Introduzindo objetos de valor (Value Objects)

Os Value Objects são objetos sem identidade conceitual e que dão característica a algum outro objeto.

Os (value objects) objetos de valor são objetos imutáveis (nenhuma alteração é permitida após a instanciação) que não possuem uma identidade própria e geralmente podem ser usados para ocupar o lugar de 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ê poderia criar um tipo de valor NumeroTelefone com validação interna, bem como métodos para extrair o código de discagem, etc.

O código abaixo mostra um objeto de valor monetário implementado como uma classe para uso com o 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 a ser realizada
            Moeda = moeda;
            Valor = valor;
        }
    }

Neste exemplo de definição de Value Objects, Moeda e Valor estão intrinsecamente ligados. Ambas as informações são necessárias para que os dados sejam úteis. Portanto, faz sentido modelá-los como tal.

Observe o uso de um construtor parametrizado e de conjuntos de propriedades set private, exatamente da mesma maneira que usamos ao modelar objetos de domínio. Um construtor privado sem parâmetros também é necessário aqui para o Entity Framework funcionar.

No contexto da 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, uma pequena adição é necessária.

No EF Core, a partir da versão 2.0, podemos usar o método OwnsOne da Fluent API:

public class BlogContext: DbContext
{
     ...
     public DbSet <BlogPost> BlogPosts {get; conjunto; }

     override protected void OnModelCreating (ModelBuilder modelBuilder)
     {
         modelBuilder.Entity <BlogPost> (). OwnsOne (x => x.Valor);
     }
}

Os benefícios da utilização dos objetos de valor são praticamente os mesmos da migração para modelos de domínio avançados. Um modelo de domínio avançado elimina a necessidade do código de chamada validar o modelo de domínio e fornece uma abstração bem definida para o programa.

Um objeto de valor se valida para que o modelo de domínio que hospeda a propriedade do objeto de valor não precise saber como validar o tipo de valor e possa ser simplificado. Tudo muito claro e simples.

Como esse é um assunto extenso e complexo vamos para por aqui.

Temos assim e 3 formas simples de enriquecer o seu modelo de domínio anêmico.

"Sujeitai-vos, pois, a Deus, resisti ao diabo, e ele fugirá de vós." Tiago 4:7

Referências:


José Carlos Macoratti