Olá!
Este artigo é o primeiro de uma série que pretende demonstrar o uso do pattern Event Sourcing (ES). Se você nunca teve contato com o pattern, começaremos juntos por aqui!
Importante! Este artigo foi fortemente inspirado pelo excelente vídeo do Elemar Jr sobre o tema. É uma introdução conceitual ao pattern com menos de 15 minutos. Recomendo que o assista e, em seguida, prossiga com a leitura deste artigo. Como o próprio Elemar Jr atenta no vídeo, a compreensão do pattern é simples, mas a implementação é bastante complexa!
Event Sourcing pra quê?
A primeira pergunta que precisamos nos fazer é: que problema o ES resolve?
O problema a resolver é a rastreabilidade do estado da sua aplicação. Ou seja, tudo o que aconteceu desde o início do seu processo de negócio até seu estado atual precisa estar registrado. Em outras palavras, sua aplicação precisa ter um foco maior no histórico do processo de negócio que no armazenamento de um modelo.
Quando não usamos ES o foco é o estado atual do seu modelo de domínio. Algo como o seguinte:
public class PriceAlert
{
...
public bool Active { get; private set; }
public void Cancel() =>
Active = false;
}
...
alert.Cancel();
Um problema desta abordagem é que, ao precisar adicionar auditoria, por exemplo, seria necessária a criação de módulo espefícico para este fim, e que todos os envolvidos no projeto se lembrassem de incluir uma chamada a este modulo a cada mudança de estado, o que seria um trabalho adicional, tanto para a auditoria em si, quanto para a manutenção e testes. Então teríamos algo como:
...
alert.Cancel();
auditService.Audit(alert.Id, DateTime.Now, $"Alerta {alert.Id} cancelado.");
Quando usamos Event Sourcing dispensamos essa necessidade, pois todos os eventos estarão naturalmente disponíveis, sem a necessidade de um módulo específico, reduzindo o trabalho necessário para registrá-lo.
Soa bem. Não? Vamos ver a partir de agora como alcançamos este resultado.
Evento: O Protagonista
Os eventos são os elementos principais deste padrão. É a partir deles que a aplicação decidirá quais mudanças de estado aplicar sobre nossas entidades, e como outros componentes do sistema poderão reagir a essas mudanças.
Para começarmos, vamos conhecer as definições de um evento. Confira o código abaixo:
namespace Lab.EventSourcing.Core
{
public interface IEvent {}
public abstract class EventBase : IEvent
{
public DateTime When { get; protected set; }
public EventBase() =>
When = DateTime.Now;
}
}
Com a interface e a classe base acima, temos uma definição básica de um evento. A interface servirá como contrato para definir um evento, e a classe base para conferir às implementações o momento em que o evento ocorreu. Desta forma, podemos avançar e adicionar suporte a eventos em nossos modelos.
Adequando o Modelo
Antes de mais nada, é necessário adicionar ao seu modelo a capacidade de registrar eventos. Para isso, será utilizada uma classe base que conterá essa capacidade, que será a seguinte:
namespace Lab.EventSourcing.Core
{
public abstract class EventSourcingModel
{
private Queue<IEvent> _pendingEvents = new Queue<IEvent>();
public IEnumerable<IEvent> PendingEvents { get => _pendingEvents.AsEnumerable(); }
protected void RaiseEvent<TEvent>(TEvent pendingEvent) where TEvent: IEvent
{
_pendingEvents.Enqueue(pendingEvent);
((dynamic)this).Apply((dynamic)pendingEvent);
}
public void Commit() =>
_pendingEvents.Clear();
}
}
Nota: Esta é uma implementação bastante simples do suporte a eventos. É comum que se encontre na Internet exemplos associados a DDD, adicionando esse suporte à uma clase base de raíz de agragado e com outras capacidades. No entanto, como o foco por ora é apenas demonstrar o ES, optamos por criar uma implementação restrita a ele e com um mínimo de funcionalidade exigido para este artigo.
Agora que temos suporte a eventos, vamos implementar nosso modelo. Ele representa um alerta de preço que será disparado uma vez que um dado ativo negociado na bolsa de valores alcance um certo preço. Veja o código abaixo:
namespace Lab.EventSourcing.PriceAlert
{
public class PriceAlert : EventSourcingModel
{
public Guid Id { get; private set; }
public string Symbol { get; private set; }
public decimal TargetPrice { get; private set; }
public DateTime TriggeredAt { get; private set; }
public bool Active { get; private set; }
public static PriceAlert Create(string symbol, decimal price)
{
if(string.IsNullOrWhiteSpace(symbol))
throw new ArgumentException("A symbol must be provided.", nameof(symbol));
if(targetPrice <= 0m)
throw new ArgumentException("A target price greater than zero must be provided.", nameof(targetPrice));
var alert = new PriceAlert();
alert.RaiseEvent(new PriceAlertCreated(new Guid(), symbol, price));
return alert;
}
public void Trigger()
{
if(TriggeredAt > DateTime.MinValue)
throw new InvalidOperationException("A price alert cannot be triggered more than once.");
if(!Active)
throw new InvalidOperationException("An inactive price alert cannot be triggered.");
RaiseEvent(new PriceAlertTriggered());
}
public void Cancel()
{
if(TriggeredAt > DateTime.MinValue)
throw new InvalidOperationException("A triggered price alert cannot be cancelled.");
if(!Active)
throw new InvalidOperationException("A price alert cannot be cancelled more than once.");
RaiseEvent(new PriceAlertCancelled());
}
internal void Apply(PriceAlertCreated pendingEvent) =>
(Id, Symbol, TargetPrice, Active) = (pendingEvent.Id, pendingEvent.Symbol, pendingEvent.TargetPrice, true);
internal void Apply(PriceAlertTriggered pendingEvent) =>
(TriggeredAt, Active) = (pendingEvent.When, false);
internal void Apply(PriceAlertCancelled pendingEvent) =>
Active = false;
}
public class PriceAlertCreated : EventBase
{
public Guid Id { get; private set; }
public string Symbol { get; private set; }
public decimal TargetPrice { get; private set; }
public PriceAlertCreated(Guid id, string symbol, decimal targetPrice) =>
(Id, Symbol, TargetPrice) = (id, symbol, targetPrice);
}
public class PriceAlertTriggered : EventBase { }
public class PriceAlertCancelled : EventBase { }
}
Vamos examinar nosso modelo para entender como ele se comunica com o ES.
Repare que, antes de tudo, ele estende nosso EventSourcingModel
para se tornar capaz de gerar eventos. Em seguida, vemos um Factory Method que retorna uma instância de nosso PriceAlert
e adiciona a ele um evento de criação com seus dados essenciais (Id, código de negociação e preço-alvo). Em seguida temos um método que aciona o disparo do alerta, Trigger
, adicionando um evento de disparo cujo horário será registrado na propriedade TriggeredAt
caso não tenha sido previamente disparado ou cancelado. E, por fim, temos um médoto de cancelamento, Cancel
que registrartá um evento de cancelamento caso o alerta ainda não tenha sido previamente disparado ou cancelado.
Logo após nosso modelo, temos os eventos por ele suportados. Todos são bastante simples, e apenas o de criação precisa carregar dados, os dados essenciais a nosso alerta de preço.
Próximos passos
Agora que temos nosso modelo suportando eventos, vamos conferir no próximo artigo como fazemos sua persistência.
Como dito no começo do artigo, a implementação é complexa e, portanto, vamos abordar um aspecto em cada artigo para tornar o aprendizado mais fácil e fluido.
E, por fim, te lanço um desafio: que tal clonar o projeto com as classes demonstradas aqui e criar testes de unidade para conferir se o estado do modelo corresponde aos eventos registrados?
Caso tenha alguma dúvida sobre o conteúdo exposto até aqui, me avise pelos comentários que respondo assim que possível. Se gostou desta introdução, me deixe saber pelos indicadores.
Até o próximo artigo!
Top comments (2)
William, burrada minha, ajustei na PR, pode validar? Abraço!
github.com/wsantosdev/lab-eventsou...
[EDIT]
Fala William! Ótimo post, ES é uma assunto que me interessa bastante! Estou fazendo os testes unitários para quando rodei o test em debug, retornou este erro por conta dos modificadores de acesso, alterei para public e deu certo:
Fala, Vinícius! Passando só pra não deixar passar em branco por aqui, já que conversamos pelo Twitter.
Mais uma vez, valeu por aceitar o desafio do teste! É muito legal ver esse angajamento.
Abração!