C# - Implementando um modelo de domínio rico


 Hoje vamos abordar conceitos básicos para a criação de um modelo de domínio rico.

Os termos modelo de domínio anêmico e modelo de domínio rico são dois termos muito freqüentes, geralmente relacionados com o DDD - Domain Driven Design , e, com microsserviços, que você vai encontrar com muita freqüência em artigos e blogs.

Nota: Eu já fiz um introdução ao modelo anêmico neste artigo - Como evitar o modelo anêmico ?

Um modelo de domínio anêmico consiste simplesmente em getters e setters (e talvez em alguns métodos simples), o que é bom para muitos cenários, mas não esta ajustado á abordagem do DDD.

Um modelo de domínio rico é um modelo que esta mais ajustado à filosofia DDD, onde temos uma classe definida com comportamentos e não apenas com gets e sets. Em um modelo de domínio rico as entidades precisam implementar o comportamento, além de implementar os atributos de dados.

Neste artigo o objetivo é apresentar conceitos básicos para criação de um modelo de domínio rico sem ir muito a fundo pois para isso seria necessário diversos artigos. Assim vou procurar ser bem objetivo indo direto ao que interessa.

Vamos supor que estou querendo criar um modelo de domínio rico para uma lógica de negócios que envolve Pedido e Item do pedido.

Vamos representar essas entidades como classes C# identificadas pelos nomes Pedido e Item definindo inicialmente um modelo anêmico mais simples possível:

  public class Pedido
  {
         int Id;
         string Nome;
         string Data;
         List<Item> Itens;
   }
  public class Item
  {
        int Id;
        string Nome;
        string Preco;
        int PedidoId;
   }
 

Acabei de definir um dos mais pobres e inexpressivos modelo de domínio possível, usando apenas campos em cada classe. Esse será o nosso ponto de partida, praticamente do zero.

Podemos identificar os seguintes problemas neste modelo anêmico:

  1. Temos neste código a quebra do primeiro pilar do paradigma da programação orientada a objetos que é o encapsulamento;
  2. A nomenclatura utilizada na definição dos nomes dos campos é pobre e confusa;
  3. Os tipos de dados usados não refletem o comportamento real dos dados expressos nos campos. Data por exemplo deveria ser um tipo DateTime e Preco do tipo decimal;

Nota: Não vou aprofundar a análise considerando usar Value Objects para Nome e Preco para não alongar demais o artigo.

Assim a primeira mudança a ser feita é usar propriedades ao invés de campos. Para embasar nossa escolha podemos citar as seguintes justificativas :

A segunda mudança será definir nomes mais adequados e também usar os tipos de dados que expressam melhor o comportamento da informação expressa.

A seguir temos o código ajustado a essas mudanças para as entidades Pedido e Item:

    public class Pedido
    {
        public int PedidoId { get; set; }
        public DateTime PedidoData { get; set; }
        public int ClienteId { get; set; }
        public List<Item> PedidoItens;
    }
    public class Item
    {
        public int ItemId { get; set; }
        public string ItemNome { get; set; }
        public decimal ItemPreco { get; set; }       
        public int PedidoId { get; set; }
    }

Em um bom design orientado a objetos, o objeto nunca pode ser definido em um estado inválido, e cabe a você como desenvolvedor pelo menos tentar evitar que isso aconteça.

Assim um item para ser válido precisa ter um valor para ItemId e também para PedidoId pois o item esta vinculado ao pedido.

Outro detalhe importante é que no código usado as propriedades estão definidas com get e set públicos, e assim, as classes estão abertas para que valores sejam atribuídos às propriedades e não temos nenhum controle sobre isso.

Assim vamos tornar todos os set privados e definir uma validação conforme as regras do negócio para que um  item seja válido e também para a coleção PedidoItens não tenha um estado inválido ou null.

Com base nestes fundamentos a seguir temos uma implementação para a classe Pedido:

using System;
using System.Collections.Generic;
namespace CShp_ModeloRico
{
    public class Pedido
    {
        public Pedido(int pedidoId, DateTime pedidoData, int clienteId )
        {
            PedidoItens = new List<Item>();

            PedidoId = pedidoId;
            PedidoData = pedidoData;
            ClienteId = clienteId;
        }
        private int _pedidoId;
        public int PedidoId
        {
            get => this._pedidoId;
            private set 
            {
                if (_pedidoId < 0)
                    throw new ArgumentNullException(nameof(PedidoId), 
                          "Código do pedido não pode ser negativo");
            }
        }
        private DateTime _pedidoData;
        public DateTime PedidoData {
            get => this._pedidoData;
            private set { 
                if(_pedidoData < DateTime.Now)
                    throw new ArgumentNullException(nameof(PedidoData),
                          "Data do pedido não pode ser anterior a data atual");
            }
        }
        private int _clienteId;
        public int ClienteId {
            get => this._clienteId;
            private set
            {
                if (_clienteId < 0)
                    throw new ArgumentNullException(nameof(ClienteId),
                          "Código do cliente não pode ser negativo");
            }
        }
        public List<Item> PedidoItens { get; set; }
    }
}

Poderíamos também ter feito uma validação para os códigos do pedido e do cliente no construtor da classe.

Agora nossa classe Pedido esta mais robusta e somente pode receber valores via construtor quando uma instância do objeto for criada e ainda estamos validando a entrada dos valores.

A seguir temos o código da classe Item onde fizemos as seguintes alterações:

Agora nossa classe Item também esta mais robusta e somente poderá receber valores via construtor da classe com uma validação de entrada.

    public class Item
    {
        public Item(int itemId, int pedidoId, string itemNome, decimal itemPreco)
        {
            if (pedidoId <= 0) throw new ArgumentException("O código do Pedido deve ser informado");
            if (itemId <= 0) throw new ArgumentException("O número do Item é obrigatório");
            PedidoId = pedidoId;
            ItemId = itemId;
            ItemNome = itemNome;
            ItemPreco = itemPreco;
        }
        public int ItemId { get; private set; }
        public int PedidoId { get; private set; }
        private string _itemNome;
        public string ItemNome {
            get => this._itemNome;
            private set {
                this._itemNome = (value.Length > 100) ? throw 
                                  new ArgumentOutOfRangeException(nameof(ItemNome),
                                  "O nome do item não pode ter mais que 100 caracteres.") : value;
            }
        }
        private decimal _itemPreco;
        public decimal ItemPreco {
            get => this._itemPreco;
            private set {
                if (_itemPreco <= 0)
                    throw new ArgumentNullException(nameof(ItemPreco),
                          "O preço do item deve ser maior que zero");
            }
        }
    }

Abaixo temos o diagrama de classe criado no VS 2019 Community.

Naturalmente esta não seria a única abordagem e as implementações feitas também não seriam as únicas, tudo vai depender das regras de negócio e comunicação entre os objetos.

Assim, apenas para mostrar um exemplo de outra abordagem, suponha que você queira ter controle no Pedido e em como os itens são incluídos no Pedido.

Nesta caso você poderia desejar validar o item primeiro e depois adicionar o item usando um método Add. E poderíamos ter um cenário onde temos pedido com o Id igual a 1, e alguém ainda faça o pedido novamente fazendo com o pedido tenha informações inválidas.

Neste cenário poderíamos ter a seguinte implementação para a classe Pedido:

using System;
using System.Collections.Generic;
namespace CShp_ModeloRico
{
    public class Pedido
    {
        public Pedido(int pedidoId, DateTime pedidoData, int clienteId )
        {
            PedidoId = pedidoId;
            PedidoData = pedidoData;
            ClienteId = clienteId;
        }
        private int _pedidoId;
        public int PedidoId
        {
            get => this._pedidoId;
            private set 
            {
                if (_pedidoId < 0)
                    throw new ArgumentNullException(nameof(PedidoId), 
                          "Código do pedido não pode ser negativo");
            }
        }
        private DateTime _pedidoData;
        public DateTime PedidoData {
            get => this._pedidoData;
            private set { 
                if(_pedidoData < DateTime.Now)
                    throw new ArgumentNullException(nameof(PedidoData),
                          "Data do pedido não pode ser anterior a data atual");
            }
        }
        private int _clienteId;
        public int ClienteId {
            get => this._clienteId;
            private set
            {
                if (_clienteId < 0)
                    throw new ArgumentNullException(nameof(ClienteId),
                          "Código do cliente não pode ser negativo");
            }
        }
        private List<Item> _items = new List<Item>();
        public IReadOnlyList<Item> PedidoItens { get { return _items; } }
        public void Add(Item item)
        {
            if (item == null) throw new ArgumentNullException("item");
            if (item.PedidoId != PedidoId) 
                 throw new InvalidOperationException("O Item não pertence a este Pedido");
            _items.Add(item);
        }
    }
}
 

Agora temos um controle maior sobre o pedido, e, se forem necessárias alterações na lista de itens, isso deve ser feito diretamente no objeto pedido.

Embora um item possa ainda ser modificado sem o conhecimento do pedido nosso design esta mais aderente às boas práticas nos levando a um código mais fácil de manter.

E estamos conversados...

"Bom é ter esperança, e aguardar em silêncio a salvação do Senhor."
Lamentações 3:26

Referências:


José Carlos Macoratti