Event Sourcing Parte 4: Domain Events

William Santos - Oct 3 '20 - - Dev Community

Olá!

Chegamos à quarta parte de nossa série sobre Event Sourcing (ES) e, a partir de agora, falaremos sobre como este padrão interage com outros e quais as consequências dessas interações para as nossas aplicações.

Neste artigo falaremos sobre Eventos de Domínio (Domain Events), que servirá de ponte para um padrão que, segundo Greg Young, propositor do ES, é indispensável quando ES é utilizado: CQRS.

Vamos lá!

Eventos? Sim! Iguais? Não!

É a partir deste ponto que a complexidade do ES se faz notar mais explicitamente.

Recuperando o que discutimos em artigos anteriores, eventos do ES representam uma mudança de estado ocorrida em um dado modelo de domínio, tendo relevância apenas em seu escopo. Ou seja, os eventos do ES só dizem respeito ao modelo que os originou.

Os eventos de domínio, por sua vez, tem como escopo todo o domínio, ou seja, não afeta o modelo que o originou, mas a outros modelos de domínio que compõem a aplicação.
Isso significa que um evento de domínio descreve uma mudança no estado do próprio processo de negócio.

Confuso? Vamos a um exemplo hipotético.

Imagine um cenário onde temos uma loja virtual simples, cujo único processo de negócio seja o da venda de produtos.
Imagine que surja o requisito de auditarmos os carrinhos de compras e que, a partir de agora, cada inserção ou remoção de um produto do carrinho, ou qualquer alteração em sua quantidade, precise ser registrada.
Como vimos nos artigos anteriores, basta criarmos eventos como ProductAdded ou ProductQuantityUpdated e teremos nossos registros de auditoria.
Agora, responda à seguinte questão: para além da auditoria, qual a relevância da manipulação de um carrinho de compras para o processo de venda de produtos? Pois é! Nenhuma! Do ponto de vista do processo, o carrinho só tem relevância quando é submetido, porque é a partir de sua submissão que outras etapas do processo serão iniciadas, como o pagamento do pedido e a separação dos produtos no estoque.

Agora, como eu notifico o domínio de que um carrinho foi submetido se seus eventos se limitam a indicar suas mudanças de estado? É aí que entram em cena os eventos de domínio!

Neste mesmo cenário, criaríamos um evento de domínio chamado CartSubmited que seria emitido para toda a aplicação, e que estaria disponível a qualquer outro modelo de domínio interessado.

Ainda soa confuso? Vamos esclarecer estes pontos apresentando seu mecanismo.

Os Três Mosqueteiros dos Eventos de Domínio

Agora que entendemos que eventos de domínio são eventos relevantes para o todo o domínio, e não para um dado modelo, entendemos também que, caso uma mudança de estado do modelo seja relevante para todo o domínio, teremos dois eventos! Um evento do modelo, que ficará restrito a seu escopo, e outro evento, este de domínio, que será distribuído aos demais modelos interessados.

Para distribuirmos um evento de domínio, precisamos de três componentes: o evento de domínio (Domain Event), que é a mensagem que será enviada; o disparador de eventos (Event Dispatcher), que se encarregará de enviar a mensagem aos interessados, e os manipuladores de eventos (Event Handlers), que tratarão este evento de domínio dando a ele um destino em um processo de negócio.

O fluxo é o seguinte: i) um dado modelo registra um evento de domínio; ii) o disparador é invocado e o evento de domínio é passado como parâmetro; iii) os manipuladores recebem esse evento do disparador e decidem como agir a partir dele.

Me mostre o código!

Para começar, falaremos sobre nosso protagonista: o Evento de Domínio.

O evento de domínio, tal como o evento do modelo, é apenas a representação de uma mensagem marcada com uma interface. Veja os exemplos abaixo.

namespace Lab.EventSourcing.DomainEvents
{
    public interface IDomainEvent { }

    ...

    public class BuyOrderCancelledDomainEvent : IDomainEvent
    {
        public Guid AccountId { get; private set; }
        public decimal Amount { get; private set; }

        public BuyOrderCancelledDomainEvent(Guid accountId, decimal amount) =>
            (AccountId, Amount) = (accountId, amount);
    }
}
Enter fullscreen mode Exit fullscreen mode

Simples. Não?

Em seguida, temos nosso disparador de eventos:

namespace Lab.EventSourcing.DomainEvents
{
    public interface IDomainEventDispatcher
    {
        void RegisterHandler<TEvent>(IDomainEventHandler handler)
            where TEvent : IDomainEvent;

        void Dispatch(IEnumerable<IDomainEvent> domainEvents);
    }

    ...

    public static class DomainEventDispatcher : IDomainEventDispatcher
    {
        private readonly ConcurrentDictionary<Type, List<IDomainEventHandler>> _handlers = 
                new ConcurrentDictionary<Type, List<IDomainEventHandler>>();

        public void RegisterHandler<TEvent>(IDomainEventHandler handler)
            where TEvent : IDomainEvent
        {
            if (_handlers.ContainsKey(typeof(TEvent)) 
                && _handlers[typeof(TEvent)].Any(h => h.GetType() == handler.GetType()))
                    throw new ArgumentException($"Handler of type {handler.GetType()} already registered.", nameof(handler));

            _handlers.AddOrUpdate(typeof(TEvent), 
                                  new List<IDomainEventHandler> { handler }, 
                                  (type, list) => { list.Add(handler); return list; });
        }

        public void Dispatch(IEnumerable<IDomainEvent> domainEvents)
        {
            if (domainEvents is null)
                throw new ArgumentNullException("A domain events collection must be provided.", nameof(domainEvents));

            foreach(var domainEvent in domainEvents)
                foreach (var handler in _handlers[domainEvent.GetType()])
                    handler.Handle(domainEvent);
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Vamos analisar nosso disparador.

Ele possui apenas dois métodos RegisterHandler, Dispatch. O primeiro é responsável registrar um IDomainEventHandler, associando-o a um dado tipo de evento. Enquanto Dispatch se encarregará de entregar a cada IDomainEventHandler a instância do evento ao qual foi relacionado.

Por fim, temos nosso manipulador de eventos, uma interface com um único método Handle, que receberá como parâmetro uma instância de IDomainEvent, o evento que será manipulado.

namespace Lab.EventSourcing.DomainEvents
{
    public interface IDomainEventHandler
    {
        void Handle(IDomainEvent domainEvent);
    }

    ...

    public class BuyOrderCreatedHandler : IDomainEventHandler
    {
        private readonly EventStore _eventStore;

        public BuyOrderCreatedHandler(EventStore eventStore) =>
            _eventStore = eventStore;

        public void Handle(IDomainEvent domainEvent)
        {
            var order = domainEvent as BuyOrderCreatedDomainEvent;
            if (order is null)
                throw new ArgumentException($"Unsuported event type {domainEvent.GetType()}.");

            var account = Account.Load(_eventStore.GetById(order.AccountId));
            account.Debit(order.Amount);

            _eventStore.Commit(account);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Agora que temos toda a infraestrutura necessária para o disparo e manipulação de eventos de domínio, precisamos adequar a ela o nosso modelo e a infraestrutura preexistente.

namespace Lab.EventSourcing.Core
{
    public abstract class EventSourcingModel<T> where T : EventSourcingModel<T>
    {
        private readonly Queue<IEvent> _pendingEvents = new Queue<IEvent>();
        public IReadOnlyCollection<IEvent> PendingEvents { get => _pendingEvents; }

        private readonly Queue<IDomainEvent> _domainEvents = new Queue<IDomainEvent>();
        public IReadOnlyCollection<IDomainEvent> DomainEvents { get => _domainEvents; }

        ...

        public void Commit()
        {
            _pendingEvents.Clear();
            _domainEvents.Clear();
        }

        protected void AddDomainEvent(IDomainEvent domainEvent) =>
            _domainEvents.Enqueue(domainEvent);
    }
}
Enter fullscreen mode Exit fullscreen mode

Repare que tanto no caso dos eventos do modelo, quanto no dos eventos de domínio, há o mesmo mecanismo: o enfileiramento de eventos e sua oferta como uma ReadonlyCollection. Entretanto, ainda que sejam muito parecidos em termos sintáticos, são completamente diferentes em termos semânticos e, por este motivo, estão separados e possuem contratos distintos (IEvent e IDomainEvent).

Agora nosso repositório de eventos também precisa ser ajustado. Isso porque será a partir dele que os eventos serão disparados. Ele ficará assim:

namespace Lab.EventSourcing.Core
{
    public class EventStore
    {
        private readonly EventStoreDbContext _eventStoreContext;
        private readonly IDomainEventDispatcher _domainEventDispatcher;

        public static EventStore Create(IDomainEventDispatcher domainEventDispatcher) =>
            new EventStore(domainEventDispatcher);

        private EventStore(IDomainEventDispatcher domainEventDispatcher)
        {
            _eventStoreContext = new EventStoreDbContext(new DbContextOptionsBuilder<EventStoreDbContext>()
                                                                .UseInMemoryDatabase(databaseName: "EventStore")
                                                                .EnableSensitiveDataLogging()
                                                                .Options);
            _domainEventDispatcher = domainEventDispatcher;
        }

        public void Commit<TModel>(TModel model) where TModel : EventSourcingModel<TModel>
        {
            ...
           _eventStoreContext.SaveChanges();

            _domainEventDispatcher.Dispatch(model.DomainEvents);

            model.Commit();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

A partir de agora, sempre que nossos eventos de modelo forem persistidos, nossos eventos de domínio serão disparados. Pode parecer estranho incluir essa responsabilidade no repositório de eventos, mas este é um atalho para garantir que não seja necessário lembrar de disparar os eventos de domínio quando um dado método for acionado -- afinal, é difícil lembrar quais métodos vão adicionar eventos de domínio além dos eventos do modelo! -- e que os eventos sejam sempre disparados, afinal de contas, o modelo sempre será persistido quando seu estado for modificado.

Pondo o conhecimento em prática.

Apesar de aparentemente difícil, a diferenciação entre eventos de modelo e eventos de domínio é bastante simples: quando um evento for necessário para que uma nova etapa do processo de negócio seja iniciada, utilize eventos de domínio além dos eventos de modelo. Quando não for necessário, utilize apenas o evento do modelo.

E, com isso, temos condições de implementar uma aplicação cujos modelos interajam por meio de eventos de domínio. Você pode clonar este repositório do Github para ter acesso ao código completo deste artigo, incluindo um projeto de testes que simula o seguinte cenário: uma corretora de valores possui contas nas quais o dinheiro do cliente é movimentado em operações com produtos financeiros. Esta conta será criada com um saldo inicial e, toda vez que o cliente enviar uma ordem de compra de ações, sua conta deve ser debitada de acordo com o financeiro desta ordem. E toda vez que uma ordem de compra for cancelada, sua conta deve ser creditada de acordo com o financeiro desta ordem.

Ou seja, sempre que uma ordem de compra for lançada, criaremos um evento de domínio que será encaminhado pelo disparador a um manipulador, que recuperará a conta do cliente e efetuará o débito em sua conta. Da mesma forma, sempre que uma ordem de compra for cancelada, um evento de domínio será lançado para que a conta seja recuperada e creditada.

Por hoje é só, pessoal! Mas antes...

É possível que você tenha notado algo importante: apesar de útil para o que se propõe, auditoria, o ES tem um problema fundamental: consultar o estado de nossos modelos a um baixo custo computacional -- afinal de contas, sempre que precisamos consultar um modelo, processamos todo o seu histórico de eventos. E, então, pode surgir a pergunta: como atendemos à interface de usuário com um custo tão alto para consultar nossos modelos? A resposta é: CQRS.

No próximo artigo apresentaremos a forma mais simples deste padrão, trazendo uma aplicação de exemplo que nos permite consultar nossos modelos como faríamos caso utilizássemos a persistência de modelos anêmicos -- porque, de certa forma, é isso que faremos!

Gostou deste artigo? Me deixe saber pelos indicadores. Ficou com alguma dúvida? Me pergunte pelos comentários, ou qualquer um dos meus contatos, que respondo assim que possível.

Até a próxima!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player