Mediator Pattern no Domain Driven Design

Mediator Pattern no Domain Driven Design

O DDD é uma abordagem de desenvolvimento de software focada no melhor entendimento das necessidades do negócio através do que ele chama de domínios. Assim há três tipos diferentes de domínios: core domain, generic domain e support domain. Esses domínios podem se comunicar por meio de integrações feitas por agregados, eventos, e afins. Porém é fundamental o correto desacoplamento entre eles. Desse modo o Mediator Pattern no Domain Driven Design descreve esse padrão e como ele ajuda a gerar um código com essa qualidade.

Relação entre os domínios

É importante entender que há diferentes tipos de domínios e que eles podem se comunicar. Em resumo, o core domain é aquela estrutura que se não existir não há o negócio da empresa; o support domain é a estrutura que ajuda o core a funcionar; já o generic é aquela estrutura que poderia ser facilmente terceirizável por não ser um diferencial do negócio. Aqui no blog temos um conjunto de artigos sobre DDD, mas destaco para o tema o Domain Driven Design Estratégico ou Desvendando o Contextmap que explicam melhor sobre os domínios.

Mas é fundamental entender que os diferentes domínios podem se relacionar e que isso não indica em específico qual arquitetura será utilizada. Portanto pode-se utilizar DDD em monolitos ou microserviços.

Comunicação

A comunicação entre os domínios é feita em por dentro dos próprios agregados ou domain services. Vamos tentar trazer uns exemplos:

Exemplo 1 – Execução de uma compra em um e-commerce

Considere que há um domínio de Checkout, outro de Estoque, outro de Delivery. São domínios distintos com grande massa de funcionalidades e estruturas para eles. Porém o evento de compra ocorre no checkout, que impacta na quantidade do Estoque. Já o estoque após reduzir a quantidade e considerar que o item não está mais disponível ele impacta no Delivery. Entenda que na prática outros domínios podem ser impactados nas operações que indicamos.

Exemplo de processo e relação entre domínios do Domain Driven Design que exemplifica o uso do Mediator Pattern.
Esse exemplo considera o Checkout, Estoque e Delivery.

Exemplo 2 – Cadastro de novo produto

Considere que um administrador pode cadastrar um novo produto. Ao fazer isso ele ficará disponível para entrega, depois para estoque e depois no catálogo de produtos. Semelhante ao exemplo um, uma cadeia de eventos deve ser disparada a medida que o primeiro evento é lançado.

Exemplo de processo e relação entre domínios do Domain Driven Design que exemplifica o uso do Mediator Pattern.
Esse exemplo considera o Controle de Produtos, Estoque, Delivery e Catálogo.

O Mediator Pattern no Domain Driven Design

O Mediator Pattern é um padrão comportamental que ajuda a lidar com as várias relações que diferentes objetos podem ter. Esse padrão é utilizado para centralizar a comunicação entre diferentes objetos de modo que um não tenha que conhecer o outro. Em contrapartida todos precisam conhecer o mecanismo de mediação.

Transportando para o Domain Driven Design é possível ver um uso desse padrão ao lidar com as relações entre agregados diferentes, seja do mesmo domínio, seja de domínios distintos. Como se trata de uma decisão específicamente arquitetural, a abrangencia do uso desse pattern não é definida pelo DDD, mas sim pelos arquitetos do software em questão.

Cenário de exemplo do uso do Pattern Mediator

Como exemplo, vamos pensar em um sistema de e-commerce que possui as entidades Produto, Carrinho e Pedido. Nesse cenário, podemos utilizar um mediador chamado CarrinhoDeComprasMediator que é responsável por coordenar as interações entre as entidades Produto e Carrinho. O CarrinhoDeComprasMediator possui as seguintes responsabilidades:

  • Adicionar um produto ao carrinho de compras
  • Remover um produto do carrinho de compras
  • Calcular o valor total do carrinho de compras
  • Criar um pedido com base nos produtos do carrinho de compras

Com isso, as entidades Produto e CarrinhoDeCompras não precisam se comunicar diretamente entre si para realizar essas tarefas. Ao invés disso, elas interagem com o mediador, que é o responsável por orquestrar as ações.

Exemplo prático em C# do Mediator simplificado

O cenário simplificado considera que o mediador conhece todos os objetos que ele precisa e faz todas as execuções necessárias. Nesse caso não é possível que um objeto em si tome uma ação que será chamada pelo mediator, mas pelo contrário, o mediator chamará os métodos necessários dos objetos.

// Entidade Produto
public class Produto
{
    public int Id { get; private set; }
    public string Nome { get; private set; }
    public decimal Preco { get; private set; }

    public Produto(int id, string nome, decimal preco)
    {
        Id = id;
        Nome = nome;
        Preco = preco;
    }
}
// Entidade Carrinho de Compras
public class CarrinhoDeCompras
{
    public List<Produto> Produtos { get; private set; }
    public decimal Total { get; private set; }

    public CarrinhoDeCompras(List<Produto> produtos)
    {
        Produtos = produtos;
    }

    public void AtualizarTotal(decimal total)
    {
        Total = total;
    }
}
// Entidade Pedido
public class Pedido
{
    public List<Produto> Produtos { get; private set; }
    public decimal Total { get; private set; }

    public Pedido(List<Produto> produtos, decimal total)
    {
        Produtos = produtos;
        Total = total;
    }
}
// Interface do Mediador CarrinhoDeComprasMediator
public interface ICarrinhoDeComprasMediator
{
    void AdicionarProduto(Produto produto, CarrinhoDeCompras carrinhoDeCompras);
    void RemoverProduto(Produto produto, CarrinhoDeCompras carrinhoDeCompras);
    void CalcularTotal(CarrinhoDeCompras carrinhoDeCompras);
    Pedido CriarPedido(CarrinhoDeCompras carrinhoDeCompras);
}
// Implementação do Mediador CarrinhoDeComprasMediator
public class CarrinhoDeComprasMediator : ICarrinhoDeComprasMediator
{
    public void AdicionarProduto(Produto produto, CarrinhoDeCompras carrinhoDeCompras)
    {
        var produtos = carrinhoDeCompras.Produtos.ToList();
        produtos.Add(produto);
        carrinhoDeCompras.Produtos = produtos;
    }

    public void RemoverProduto(Produto produto, CarrinhoDeCompras carrinhoDeCompras)
    {
        var produtos = carrinhoDeCompras.Produtos.ToList();
        produtos.Remove(produto);
        carrinhoDeCompras.Produtos = produtos;
    }

    public void CalcularTotal(CarrinhoDeCompras carrinhoDeCompras)
    {
        var total = carrinhoDeCompras.Produtos.Sum(p => p.Preco);
        carrinhoDeCompras.AtualizarTotal(total);
    }

    public Pedido CriarPedido(CarrinhoDeCompras carrinhoDeCompras)
    {
        return new Pedido(carrinhoDeCompras.Produtos, carrinhoDeCompras.Total);
    }
}
// Exemplo de uso
public class Exemplo
{
    private readonly ICarrinhoDeComprasMediator _carrinhoDeComprasMediator;

    public Exemplo(ICarrinhoDeComprasMediator carrinhoDeComprasMediator)
    {
        _carrinhoDeComprasMediator = carrinhoDeComprasMediator;
    }

    public void AdicionarProdutoAoCarrinho(Produto produto, CarrinhoDeCompras carrinhoDeCompras)
    {
        _carrinhoDeComprasMediator.AdicionarProduto(produto, carrinhoDeCompras);
    }

    public void RemoverProdutoDoCarrinho(Produto produto, CarrinhoDeCompras carrinhoDeCompras)
    {
        _carrinhoDeComprasMediator.RemoverProduto(produto, carrinhoDeCompras);
    }

    public void CalcularTotalDoCarrinho(CarrinhoDeCompras carrinhoDeCompras)
    {
        _carrinhoDeComprasMediator.CalcularTotal(carrinhoDeCompras);
    }

    public Pedido CriarPedido(CarrinhoDeCompras carrinhoDeCompras)
    {
        return _carrinhoDeComprasMediator.CriarPedido(carrinhoDeCompras);
    }
}

Exemplo de C# com Mediator partindo dos objetos

Esse exemplo é um pouco diferente do anterior por que os objetos são capazes de chamar o método Notify que, por sua vez, farão parte do Mediator. Esse caso é um pouco mais complexo e mais robusto do que o exemplo anterior.

// Interface IMediator
public interface IMediator
{
    void Notify(BaseEntity entity, string operation);
}
// Classe abstrata utilizada pelas entidades
//    as classes filhas dessa possuem acesso ao Mediator
public abstract class BaseEntity
{
    protected IMediator Mediator { get; }

    protected BaseEntity(IMediator mediator)
    {
        Mediator = mediator;
    }
}
// Entidade Produto que herda de BaseEntity
//   Ele faz uso do método Notify do Mediator
public class Produto : BaseEntity
{
    public int Id { get; }
    public string Nome { get; }
    public decimal Preco { get; }

    public Produto(int id, string nome, decimal preco, IMediator mediator) : base(mediator)
    {
        Id = id;
        Nome = nome;
        Preco = preco;
    }

    public void Adicionar()
    {
        Mediator.Notify(this, "adicionar");
    }

    public void Remover()
    {
        Mediator.Notify(this, "remover");
    }
}
// Entidade CarrinhoDeCompras que herda de BaseEntity
//   Ele faz uso do método Notify do Mediator
public class CarrinhoDeCompras : BaseEntity
{
    public int Id { get; }
    public List<Produto> Produtos { get; }
    public decimal Total { get; set; }

    public CarrinhoDeCompras(int id, IMediator mediator) : base(mediator)
    {
        Id = id;
        Produtos = new List<Produto>();
    }

    public void AdicionarProduto(Produto produto)
    {
        Mediator.Notify(this, "adicionarProduto");
        produto.Adicionar();
    }

    public void RemoverProduto(Produto produto)
    {
        Mediator.Notify(this, "removerProduto");
        produto.Remover();
    }

    public void CalcularTotal()
    {
        Mediator.Notify(this, "calcularTotal");
    }

    public void CriarPedido()
    {
        Mediator.Notify(this, "criarPedido");
    }
}
// Mediador em si
//   Implementa o método Notify com as ações para o que vem dos métodos Notify das Entidades
//   Também possui propriedades próprias como o List<Pedidos>
public class CarrinhoDeComprasMediator : IMediator
{
    public List<Produto> Produtos { get; set; }
    public List<CarrinhoDeCompras> Carrinhos { get; set; }
    public List<Pedido> Pedidos { get; set; }

    public CarrinhoDeComprasMediator()
    {
        Produtos = new List<Produto>();
        Carrinhos = new List<CarrinhoDeCompras>();
        Pedidos = new List<Pedido>();
    }

    public void Notify(BaseEntity entity, string operation)
    {
        if (entity is Produto)
        {
            if (operation == "adicionar")
            {
                Produtos.Add((Produto)entity);
            }
            else if (operation == "remover")
            {
                Produtos.Remove((Produto)entity);
            }
        }
        else if (entity is CarrinhoDeCompras)
        {
            if (operation == "adicionarProduto")
            {
                var carrinho = (CarrinhoDeCompras)entity;
                carrinho.Produtos.Add((Produto)entity);
            }
            else if (operation == "removerProduto")
            {
                var carrinho = (CarrinhoDeCompras)entity;
                carrinho.Produtos.Remove((Produto)entity);
            }
            else if (operation == "calcularTotal")
            {
                var carrinho = (CarrinhoDeCompras)entity;
                carrinho.Total = carrinho.Produtos.Sum(p => p.Preco);
            }
            else if (operation == "criarPedido")
            {
                var carrinho = (CarrinhoDeCompras)entity;
                var pedido = new Pedido(carrinho.Id, carrinho.Produtos, carrinho.Total);
                Pedidos.Add(pedido);
            }
        }
    }
}
// Classe pedido, utilizada específicamente pelo Mediator
public class Pedido
{
    public int Id { get; }
    public List<Produto> Produtos { get; }
    public decimal Total { get; }

    public Pedido(int id, List<Produto> produtos, decimal total)
    {
        Id = id;
        Produtos = produtos;
        Total = total;
    }
}
// Exemplo de uso do mediator
public class Exemplo
{
    public void Executar()
    {
        var mediator = new CarrinhoDeComprasMediator();

        var produto1 = new Produto(1, "Produto 1", 100, mediator);
        var produto2 = new Produto(2, "Produto 2", 200, mediator);

        var carrinho1 = new CarrinhoDeCompras(1, mediator);
        var carrinho2 = new CarrinhoDeCompras(2, mediator);

        carrinho1.AdicionarProduto(produto1);
        carrinho1.AdicionarProduto(produto2);

        carrinho1.CalcularTotal();
        carrinho2.AdicionarProduto(produto2);
        carrinho2.CalcularTotal();
        carrinho1.CriarPedido();
        carrinho2.CriarPedido();

        foreach (var pedido in mediator.Pedidos)
        {
            Console.WriteLine($"Pedido {pedido.Id}: Total {pedido.Total:C}");
        }
    }
}

Mediator com bibliotecas

A implementação do Mediator não é muito complexa mas pode deixar o código com muita dependência de uma só classe. A classe mediadora corre o risco de se tornar uma “God Class”. Sabendo disso é importante ter em mente que é fundamental ter uma boa estratégia na implementação do pattern para evitar maiores problemas. E para tal há bibliotecas prontas que ajudam nesses aspectos.

A linguagem que utilizamos nos exemplos é C#, mas o que trazemos aqui deve ser verdadeiro para outras linguagens ou plataformas. Em .NET há uma biblioteca chamada mediatR criada por Jimmy Bogard com o intuíto de tornar a implementação do mediator mais simples e prática.

O exemplo a seguir é bem simplificado para demonstrar friamente como o mediatR pode ser utilizado. Essa biblioteca reduz drasticamente a implementação do método Notify(BaseEntity entity, string operation) visto anteriormente. Note que ele tinha um monte de IFs e que cresceriam vertiginosamente a medida que mais entidades iam fazendo parte da intermediação.

// Estrutura do arquivo Program.cs de uma aplicação Webapi em .NET 6
// Nela há a configuração específica da Injeção de dependências do MediatR
using System.Reflection;
using MediatR;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddMediatR(cfg=>cfg.RegisterServicesFromAssemblies(Assembly.GetExecutingAssembly()));
...
// Código da Controller MediadorController
// Ela tem o construtor que é definido dinâmicamente pela injeção
// Ela dá suporte ao uso do IMediator<T> para que seja possível consumir o Mediator
using MediatR;
using Microsoft.AspNetCore.Mvc;

namespace Presentation.Controllers;

[ApiController]
[Route("[controller]")]
public class MediadorController : ControllerBase
{
 
    IMediator _mediator;
    public MediadorController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpGet(Name = "CriarProduto")]
    public async Task<ProductEntity> CriarProduto(string nomeDoProduto)
    {
        return await _mediator.Send(new ProductRequest(new ProductEntity(nomeDoProduto)));       
    }

}
// Entidade ProductEntity é um exemplo que pode ser aplicado ao cenário
public class ProductEntity
{
    public string Nome{get; set;}
    public ProductEntity(string nome)
    {
        this.Nome = nome;
    }
}
// Esse é o ProductRequest, utilizado pela controller acima
// Ele implementa o IRequest<T> que não possui nenhum método a ser implementado, mas é constraint para o próximo código
using MediatR;

public class ProductRequest : IRequest<ProductEntity>
{
    public ProductRequest(ProductEntity product)
    {
        this.Producting = product;
    }
    public ProductEntity Producting{get; set;}
}
// Esse é o Handler, que carrega a execução do ProductRequest
// Dinamicamente o Mediator descobre que esse Handler responde ao Request indicador, por conta das interfaces implementadas
using MediatR;

public class ProductHandler : IRequestHandler<ProductRequest, ProductEntity>
{
    public Task<ProductEntity> Handle(ProductRequest request, CancellationToken cancellationToken)
    {
        return Task.FromResult<ProductEntity>(request.Producting);
    }
}

Conclusão de Mediator Pattern no Domain Driven Design

O padrão mediator é relativamente simples de entender mas pode se tornar complexo de manter. Mas além disso ele é muito conveniente quando se trabalha com Domain Driven Design. Assim é possível isolar agregados e garantir comunicação entre eles sem acoplamento: Entenda que isso independe se é utilizada a arquitetura monolitica ou de microserviços. Desse modo o artigo Mediator Pattern no Domain Driven Design demonstrou cenários práticos, diferentes formas de implementar, com frameworks de terceiros ou sem. Embora os exemplos estejam numa linguagem específica de programação, eles são facilmente compreensíveis e portáveis para outras plataformas ou linguagens.


Thiago Anselme
Thiago Anselme - Gerente de TI - Arquiteto de Soluções

Ele atua/atuou como Dev Full Stack C# .NET / Angular / Kubernetes e afins. Ele possui certificações Microsoft MCTS (6x), MCPD em Web, ITIL v3 e CKAD (Kubernetes) . Thiago é apaixonado por tecnologia, entusiasta de TI desde a infância bem como amante de aprendizado contínuo.

Deixe um comentário