DDD - Agregados na prática


  Hoje vou apresentar o conceito de Agregados (Aggregates) e como usá-los na prática na plataforma .NET.

O Domain-Driven Design (DDD) é uma metodologia poderosa para construir sistemas de software complexos que representam de perto o domínio do mundo real que atendem.



Um dos conceitos fundamentais em DDD são os Aggregates ou Agregados, que desempenham um papel central na organização e gerenciamento do modelo de domínio.

Neste artigo, vou apresentar os conceitos básicos sobre os Agregados seu significado.

O que é um Agregado

Um Agregado é uma estrutura de modelagem que define um conjunto de entidades e objetos de valor que são tratados como uma única unidade dentro do domínio de um aplicativo. O principal objetivo de um Agregado é garantir a consistência e a integridade dos dados no domínio, controlando o acesso e a modificação das entidades e objetos de valor relacionados.

É
importante notar que, embora um Agregado possa conter várias entidades e objetos de valor, ele tem uma raiz que atua como o ponto de entrada para o Agregado e é responsável por controlar o acesso aos elementos internos.

A raiz do Agregado é conhecida como  Raiz Agregada

O que é uma Raiz Agregada ?

Cada agregado possui um ponto de entrada designado, conhecido como raiz agregada. A Raiz Agregada controla o acesso aos objetos dentro do Agregado e impõe invariantes e regras de negócios dentro do domínio. Este conceito fornece uma maneira de encapsular objetos de domínio relacionados, garantindo consistência e integridade dos dados.

No DDD, determinar os limites dos agregados e suas raízes agregadas é uma decisão de design crucial e tudo depende do caso de uso de negócios. No entanto, os seguintes cenários podem ajudar a orientar a decisão de criar agregados separados ou usar um agregado existente:

A função do Aggregate Root ou Raiz Agregada inclui:

  1. Controlar o Acesso: A Agregado Root é a única parte do Agregado que pode ser acessada diretamente a partir do exterior. Todas as operações no Agregado, incluindo a leitura e a escrita, ocorrem por meio da raiz.
  2. Garantir Consistência: A raiz é responsável por garantir que todas as operações dentro do Agregado sejam consistentes e cumpram as regras de negócio.
  3. Gerenciar Eventos de Domínio: A raiz é frequentemente responsável por emitir eventos de domínio para notificar outras partes do sistema sobre as mudanças no Agregado.

Ciclo de vida de um  Agregado

No Domain-Driven Design (DDD), um Agregado não é apenas uma estrutura estática de dados; ele possui um ciclo de vida bem definido, que abrange desde a sua criação até a sua persistência e, eventualmente, a sua remoção. A Raiz Agregada (Aggregate Root) é a guardiã desse ciclo, sendo o único ponto de entrada para todas as operações que afetam o estado do Agregado.

1. Criação do Agregado

O ciclo de vida de um Agregado começa com a sua criação. Isso sempre ocorre através da sua Raiz Agregada. Você nunca deve criar uma entidade interna de um Agregado de forma isolada e depois tentar "anexá-la" à Raiz Agregada. A criação deve respeitar os invariantes do Agregado desde o primeiro momento.

 -  Construtor da Raiz Agregada: O construtor da Raiz Agregada é o local ideal para inicializar o Agregado, garantir que todos os dados obrigatórios estejam presentes e que os invariantes iniciais sejam satisfeitos.

 -  Fábricas (Factories): Para lógicas de criação mais complexas (por exemplo, quando um Agregado depende de outros para ser criado), você pode usar um objeto Fábrica de Domínio. A Fábrica encapsula a complexidade da criação, garantindo que o Agregado seja instanciado em um estado válido.

Exemplo:  Ao criar um Pedido (Order), você o faz através do construtor da classe Order, que é a Raiz Agregada. Esse construtor pode receber informações essenciais, como o ShippingAddress e garantir que o pedido tenha um OrderId e OrderDate válidos.

public class Order
{
    public Guid OrderId { get; private set; }
    public DateTime OrderDate { get; private set; }
    public List<OrderItem> Items { get; private set; }
    public ShippingAddress ShippingAddress { get; private set; }
    // Construtor é o ponto de criação da Aggregate Root
    public Order(ShippingAddress shippingAddress)
    {
        OrderId = Guid.NewGuid();
        OrderDate = DateTime.UtcNow;
        Items = new List<OrderItem>();
        ShippingAddress = shippingAddress;
        // Aqui você pode adicionar validações iniciais (invariantes)
        if (shippingAddress == null)
        {
            throw new ArgumentNullException(nameof(shippingAddress),
                   "Shipping address is required to create an order.");
        }
    }
    // ... outros métodos
}

2. Modificação do Agregado

Após a criação, o Agregado pode ser modificado. Todas as alterações no estado interno do Agregado (sejam na Raiz Agregada, nas entidades internas ou nos objetos de valor) devem ser orquestradas e validadas pela Raiz Agregada. Isso garante que os invariantes do Agregado sejam mantidos em todas as operações.

 - Métodos Comportamentais: As modificações não são feitas diretamente em propriedades públicas; em vez disso, são feitas através de métodos bem definidos na Raiz Agregada que expressam a intenção do domínio. Esses métodos encapsulam a lógica de negócio e as validações.

 - Eventos de Domínio: Durante uma modificação, a Raiz Agregada pode optar por publicar Eventos de Domínio para notificar outras partes do sistema sobre o que aconteceu. Isso é crucial para comunicação entre Agregados e para desacoplamento.

Exemplo: Adicionar um item a um Pedido é feito através do método AddItem na classe Order. Esse método pode conter lógicas de validação (ex: quantidade mínima, estoque disponível) antes de adicionar o item.

public class Order
{
    // ... propriedades e construtor
    // Método para adicionar um item ao pedido, controlando a lógica de negócio
    public void AddItem(Guid productId, int quantity, decimal unitPrice)
    {
        if (quantity <= 0)
        {
            throw new ArgumentException("Quantity must be positive.");
        }
        // Exemplo de invariante: verificar se o preço total do pedido não excede um limite
        // decimal currentTotal = Items.Sum(item => item.Quantity * item.UnitPrice);
        // if (currentTotal + (quantity * unitPrice) > MAX_ORDER_VALUE)
        // {
        //     throw new InvalidOperationException("Order total exceeds maximum allowed value.");
        // }
        Items.Add(new OrderItem
        {
            ProductId = productId,
            Quantity = quantity,
            UnitPrice = unitPrice
        });
        // Opcionalmente, emitir um evento de domínio:
        // DomainEvents.Raise(new OrderItemAdded(OrderId, productId, quantity, unitPrice));
    }
    public void ChangeShippingAddress(ShippingAddress newAddress)
    {
        if (newAddress == null)
        {
            throw new ArgumentNullException(nameof(newAddress));
        }
        this.ShippingAddress = newAddress;
        // Opcionalmente, emitir um evento de domínio:
        // DomainEvents.Raise(new ShippingAddressChanged(OrderId, newAddress));
    }
}

3. Persistência e Recuperação

Agregados são a unidade de persistência transacional. Isso significa que quando você salva um Agregado, todas as suas partes internas (Raiz Agregada, entidades internas e objetos de valor) são salvas juntas como uma única transação atômica. Se qualquer parte falhar, toda a transação é revertida, garantindo a consistência.

 - Repositórios: A responsabilidade de persistir e recuperar Agregados é dos Repositórios. Um Repositório é uma coleção de Agregados do mesmo tipo, e ele sempre lida com o Agregado como uma unidade completa.

 - Atomicidade: A garantia de que o Agregado é salvo ou falha como uma unidade é fundamental para a integridade dos dados.

Exemplo: Um OrderRepository seria responsável por salvar e buscar instâncias de Order.

// Exemplo de um Repositório (interface)
public interface IOrderRepository
{
    Order GetById(Guid orderId);
    void Save(Order order);
    // Pode ser implementado se a regra de negócio permitir a remoção
    void Remove(Order order); }
// Exemplo de uso
// var orderRepository = new OrderRepository();
// var order = orderRepository.GetById(orderId); // Carrega o Agregado completo
// order.AddItem(productId, quantity, unitPrice); // Modifica o Agregado
// orderRepository.Save(order); // Salva o Agregado completo e suas modificações

4. Remoção do Agregado

A remoção de um Agregado também deve ser feita através da sua Raiz Agregada e, consequentemente, via Repositório. Assim como na criação e modificação, a remoção é uma operação transacional que afeta todas as partes do Agregado como uma unidade.

  - Comportamento de Negócio: A decisão de remover um Agregado geralmente é um comportamento de negócio, e não apenas uma exclusão de dados. Por exemplo, um Pedido só pode ser "cancelado" se estiver em um determinado status. Essa lógica deve ser encapsulada.

  - Impacto Cascata: A remoção da Raiz Agregada implica na remoção de todas as entidades e objetos de valor que pertencem a ele.

Compreender o ciclo de vida de um Agregado é crucial para modelar o domínio de forma eficaz, garantindo a integridade dos dados e um design robusto e maleável.

Quando incluir entidades dentro do agregado existente?

Uma Raiz Agregada define um limite transacional dentro do qual as mudanças devem ocorrer atomicamente. Em outras palavras, qualquer modificação no estado de um Agregado, incluindo a própria Raiz Agregada e suas entidades associadas, deve ser totalmente bem-sucedida ou falhar. Isso garante que os dados no agregado permaneçam em um estado consistente.

As Invariantes são as regras que devem ser mantidas dentro do Agregado para garantir a integridade dos dados. Esses invariantes representam regras ou condições de negócios que devem ser sempre verdadeiras.

Por exemplo, se você estiver modelando um sistema de comércio eletrônico, uma invariante pode ser que o preço total de todos os itens do pedido não deva aumentar pelo valor máximo permitido no limite do pedido. Portanto, o Pedido e o Item do Pedido geralmente fazem parte do mesmo agregado

Como regra geral, toda vez que quisermos operar em uma Raiz Agregada, devemos recuperar o objeto completo.

Como exemplo,  suponha que estamos criando um sistema de comércio eletrônico e precisamos modelar a entidade "Pedido" com seus detalhes, como "Item de Pedido" e "Endereço de Entrega".

Vamos aplicar o conceito de Agregados e Rais Agregada.

// Entidade de Item de Pedido
public class OrderItem
{
    public Guid ProductId { get; set; }
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
}
// Objeto de Valor para Endereço de Entrega
public class ShippingAddress
{
    public string? Street { get; set; }
    public string? City { get; set; }
    public string? PostalCode { get; set; }
}
// Agregado Root: Pedido
public class Order
{
    public Guid OrderId { get; private set; }
    public DateTime OrderDate { get; private set; }
    public List<OrderItem> Items { get; private set; }
    public ShippingAddress ShippingAddress { get; private set; }
    public Order(ShippingAddress shippingAddress)
    {
        OrderId = Guid.NewGuid();
        OrderDate = DateTime.UtcNow;
        Items = new List<OrderItem>();
        ShippingAddress = shippingAddress;
    }
    // Método para adicionar um item ao pedido
    public void AddItem(Guid productId, int quantity, decimal unitPrice)
    {
        Items.Add(new OrderItem
        {
            ProductId = productId,
            Quantity = quantity,
            UnitPrice = unitPrice
        });
    }
}

Neste exemplo:

A Raiz Agregada Order controla o acesso aos elementos internos, garantindo a consistência e a integridade dos dados. Este é um exemplo simplificado, mas ilustra o conceito de Agregados e como eles ajudam a modelar o domínio de um sistema de forma coesa e consistente.

A seguir um exemplo bem básico usando o Agregado :

class Program
{
    static void Main()
    {
        // Criação de um novo pedido com um endereço de entrega
        var shippingAddress = new ShippingAddress
        {
            Street = "123 Main St",
            City = "Exampleville",
            PostalCode = "12345"
        };
        var order = new Order(shippingAddress);
        // Adicionando itens ao pedido
        order.AddItem(Guid.NewGuid(), 2, 25.00M);
        order.AddItem(Guid.NewGuid(), 1, 10.00M);
       // Agora, o pedido encapsula os itens de pedido e o endereço
       //  de entrega como um Agregado.
       // Todas as operações no pedido são realizadas por meio da raiz 
       //  do Agregado, que é a própria instância do pedido.
    }
}

Para concluir , aqui estão os principais pontos relacionados ao conceito de Agregado:

  1. Unidade Transacional: Um Agregado é uma unidade transacional, o que significa que todas as operações dentro de um Agregado devem ser consistentes juntas. Isso implica que as operações que envolvem várias entidades ou objetos de valor dentro de um Agregado devem ser tratadas como uma única transação.
     
  2. Acesso Controlado: As entidades e objetos de valor dentro de um Agregado são acessados e modificados somente por meio da Raiz do Agregado, chamada de Raiz Agregada. Isso garante que todas as operações no Agregado sejam consistentes, já que a raiz controla o acesso aos elementos internos.
     
  3. Consistência Garantida: O Agregado é projetado de forma a garantir que todas as regras de negócio e invariâncias sejam mantidas em todos os momentos. Isso impede que o domínio entre em estados inconsistentes.
     
  4. Agregação de Elementos: Um Agregado pode consistir em várias entidades e objetos de valor. Por exemplo, em um sistema de gerenciamento de pedidos, um Agregado de Pedido pode conter entidades como Itens de Pedido e objetos de valor como Endereço de Entrega.
     
  5. Identificador Global Único: Cada Raiz Agregada tem um identificador global único que o diferencia de outros Agregados. Isso facilita a identificação e o acesso aos Agregados no sistema.

É importante lembrar que os Agregados são definidos e vivem dentro de um Contexto Delimitado, que é um limite explícito de um modelo de domínio específico, garantindo que as regras de negócio de um Agregado sejam consistentes dentro desse contexto."

E estamos conversados...

"Ouve tu então nos céus, assento da tua habitação, e perdoa, e age, e dá a cada um conforme a todos os seus caminhos, e segundo vires o seu coração, porque só tu conheces o coração de todos os filhos dos homens."
1 Reis 8:39

Referências:


José Carlos Macoratti