Domain Driven Design - Usando Repositórios


 Hoje veremos o conceito de Repositório na abordagem do Domain Driven Design - DDD.

No Domain Driven Design, os repositórios desempenham um papel crucial na facilitação do acesso ao sistema subjacente de persistência de dados, como um banco de dados, para ler e escrever objetos de negócios, particularmente agregados (aggregates)

O que é um repositório

Um repositório pode ser visto como uma interface semelhante a uma coleção que atua como uma ponte entre as camadas de domínio e de aplicação, fornecendo uma maneira padronizada de interagir com a camada de persistência de dados. Ele encapsula a lógica necessária para executar operações CRUD (Criar, Ler, Atualizar, Excluir) em entidades comerciais.

Princípios do Repositório

Para implementar repositórios de forma eficaz usando DDD, é essencial aderir a um conjunto de princípios comuns. Vejamos as principais:

1. Defina uma interface de repositório na camada de domínio

Os repositórios devem ser definidos como interfaces na camada de domínio pois elas representam contratos que definem como os objetos no domínio podem ser acessados e manipulados.
 Ao fazer isso, garantimos que os repositórios possam ser utilizados tanto pelas camadas de domínio quanto de aplicação. A interface serve como um contrato que define as operações disponíveis para acessar e manipular objetos de negócios.

Exemplo de definição de interface para repositório de produto:

    public interface IProductRepository
    {
        Product GetById(Guid productId);
        void Add(Product product);
        void Update(Product product);
        void Remove(Product product);
    }

As interfaces são implementadas na camada de Infraestrutura e implementações dos repositórios são responsáveis por traduzir as operações definidas nas interfaces em ações específicas no armazenamento de dados. Aqui são incluídas tecnologias para acesso a dados como o Entity Framework Core.

    public class ProductRepository : IProductRepository
    {
        private readonly DbContext _context;

        public ProductRepository(DbContext context)
        {
            _context = context;
        }

        public Product GetById(Guid productId)
        {
                      // implementação
        }

        public void Add(Product product)
        {
                       // implementação
        }

        public void Update(Product product)
        {
                       // implementação
        }

        public void Remove(Product product)
        {
                        // implementação
        }
    }

Lembre-se de que a camada Application (onde estão os casos de uso ou use cases) interage com os repositórios por meio das interfaces definidas na camada de domínio. Isso mantém a lógica de aplicação desacoplada da infraestrutura de armazenamento de dados, facilitando a testabilidade e a manutenção do código.

Evite incluir lógica de negócios em repositórios

Os repositórios devem se concentrar exclusivamente em tarefas de acesso e manipulação de dados. É importante mantê-los livres de qualquer lógica de negócios. As regras e validações de negócios devem ser tratadas pelas entidades e serviços do domínio, garantindo uma separação clara de preocupações.

A interface do repositório deve ser independente do provedor de banco de dados/ORM

Para promover flexibilidade e evitar o acoplamento dos repositórios a um provedor de banco de dados específico ou a uma estrutura de Mapeamento Objeto-Relacional (ORM), a interface do repositório não deve expor tipos específicos do provedor. Por exemplo, é recomendado evitar o retorno de um DbSet (fornecido pelo EF Core) de um método de repositório.

Crie repositórios para Aggregate Roots

No DDD, os repositórios são normalmente criados para Aggregate Roots, em vez de entidades individuais. Um aggregate root ou raiz agregada representa um cluster de entidades associadas que devem ser tratadas como uma única unidade. O acesso às entidades da subcoleção dentro de um agregado deve ser feito através da raiz agregada, garantindo consistência e limites transacionais.

Suponha que estamos trabalhando em um sistema de e-commerce e temos um Aggregate Root chamado Order que gerencia pedidos de clientes.

Vamos começar definindo a classe Order como nosso Aggregate Root no namespace de domínio ou seja no projeto Domain:

   public class Order
    {
        public int Id { get; private set; }
        public DateTime OrderDate { get; private set; }
        private List<OrderItem> _orderItems = new List<OrderItem>();
        public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly();
        // Construtores, métodos de domínio e validações aqui
    }
    public class OrderItem
    {
        public int Id { get; private set; }
        public int ProductId { get; private set; }
        public decimal Quantity { get; private set; }
        public decimal Price { get; private set; }
        // Construtores e métodos de domínio aqui
    }

Lembrando que as entidades dentro do Aggregate devem ser acessadas através do Aggregate Root, e elas devem ter visibilidade privada ou protegida (protected) para garantir que não sejam acessadas diretamente de fora do Aggregate.

Por isso as propriedades de OrderItems foram definidas para serem de apenas leitura (IReadOnlyCollection<OrderItem>) para garantir que a lista de itens do pedido não seja modificada diretamente de fora da classe Order.

Apenas a título de ilustração, um esboço para o aggregate Order contendo os métodos de domínio para incluir e remover um item de um pedido poderia ser feita da seguinte forma:

public class Order
{
    public int Id { get; private set; }
    public DateTime OrderDate { get; private set; }
    private List<OrderItem> _orderItems = new List<OrderItem>();
    public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly();

    // Construtor para criar um novo pedido
    public Order(DateTime orderDate)
    {
        // Realize validações aqui, se necessário.
        Id = GenerateUniqueId();
        OrderDate = orderDate;
    }

    // Método de domínio para adicionar um item ao pedido
    public void AddOrderItem(int productId, decimal quantity, decimal price)
    {
        // Realize validações aqui, se necessário.
        var orderItem = new OrderItem(productId, quantity, price);
        _orderItems.Add(orderItem);
    }

    // Método de domínio para remover um item do pedido
    public void RemoveOrderItem(int orderItemId)
    {
        // Realize validações aqui, se necessário.
        var orderItemToRemove = _orderItems.SingleOrDefault(item => item.Id == orderItemId);
        if (orderItemToRemove != null)
        {
            _orderItems.Remove(orderItemToRemove);
        }
    }

    // Outros métodos de domínio e validações aqui

    // Métodos privados para encapsular a lógica de geração de ID único, se necessário
    private int GenerateUniqueId()
    {
        // Lógica para gerar um ID único aqui
    }
}

public class OrderItem
{
    public int Id { get; private set; }
    public int ProductId { get; private set; }
    public decimal Quantity { get; private set; }
    public decimal Price { get; private set; }

    public OrderItem(int productId, decimal quantity, decimal price)
    {
        // Realize validações aqui, se necessário.
        Id = GenerateUniqueId();
        ProductId = productId;
        Quantity = quantity;
        Price = price;
    }

    // Outros métodos de domínio e validações aqui

    // Métodos privados para encapsular a lógica de geração de ID único, se necessário
    private int GenerateUniqueId()
    {
        // Lógica para gerar um ID único aqui
    }
}

Desta forma, ao definir o seu Aggregate Root procure levar em conta as seguintes recomendações:

  1. Garanta visibilidade privada ou protegida para as propriedades das entidades dentro do Aggregate. Por este movito as propriedades como OrderItems na classe Order devem ter visibilidade privada (private) ou protegida (protected) para impedir que sejam acessadas diretamente de fora da classe.
  2. Forneça métodos no Aggregate Root (no caso, na classe Order) para realizar operações relacionadas ao Aggregate e manter sua consistência. Por exemplo, métodos para adicionar ou remover itens do pedido, calcular o valor total do pedido, etc.
  3. Certifique-se de que a criação e a validação do Aggregate sejam tratadas em seus construtores ou em métodos de fábrica adequados.
  4. Considere como você lidará com a persistência do Aggregate no repositório. Você precisará de métodos para carregar e salvar o Aggregate Root e, opcionalmente, as entidades relacionadas.
  5. Lembre-se de que o Aggregate Root é a única entrada para o Aggregate, então qualquer operação no Aggregate deve ser realizada através dele.
  6. Certifique-se de que os métodos de domínio e as validações estejam bem implementados para garantir a consistência do Aggregate.

Agora, vamos criar a interface do repositório que define as operações que o repositório deve fornecer para acessar e manipular os Orders.

    public interface IOrderRepository
    {
        Task<Order> GetByIdAsync(int id);
        Task AddAsync(Order order);
        Task UpdateAsync(Order order);
        Task DeleteAsync(Order order);
        // Outras operações específicas do Aggregate Root
    }

A implementação do repositório será feita na camada de infraestrutura, e, esta implementação se conectará ao banco de dados ou outro meio de armazenamento de dados.

public class OrderRepository : IOrderRepository
    {
        private readonly DbContext _context;
        public OrderRepository(DbContext context)
        {
            _context = context;
        }
        public async Task<Order> GetByIdAsync(int id)
        {
            return await _context.Orders.FindAsync(id);
        }
        public async Task AddAsync(Order order)
        {
            await _context.Orders.AddAsync(order);
        }
        public async Task UpdateAsync(Order order)
        {
            _context.Orders.Update(order);
        }
        public async Task DeleteAsync(Order order)
        {
            _context.Orders.Remove(order);
        }
        // Implementações de outras operações específicas do Aggregate Root
    }

Neste exemplo, o OrderRepository é responsável por acessar e manipular os objetos Order no banco de dados. As operações definidas na interface do repositório (como GetByIdAsync, AddAsync, UpdateAsync, DeleteAsync) refletem as principais operações que podem ser realizadas no Aggregate Root.

Lembre-se de que as classes de entidades e repositórios são parte da camada de domínio. A camada de aplicação (Application Layer) usará o repositório para acessar e manipular os Aggregates Roots, encapsulando a lógica de negócios e permitindo que o domínio permaneça independente da implementação da infraestrutura.

Desta forma os repositórios devem se concentrar principalmente no acesso e manipulação de dados, deixando a lógica do domínio para as entidades e serviços do domínio.

E estamos conversados...

"Se confessarmos os nossos pecados, ele é fiel e justo para nos perdoar os pecados, e nos purificar de toda a injustiça."
1 João 1:9

Referências:


José Carlos Macoratti