Eventos de Domínio – Disparo e Consumo

No artigo anterior falamos sobre a criação e utilização de eventos de domínio. O artigo abordou até o momento do disparo do evento propriamente dito, através da classe DomainEvents, só que sem mostrar detalhes de sua implementação. Existem diversas formas de se implementar o método de disparo do evento, mas antes de falarmos sobre estas técnicas, precisamos abordar como se constrói os consumidores dos eventos.

Para especificarmos os tratadores, vamos criar uma interface que descreverá apenas um método: Handler. Só que trata-se de uma interface genérica, onde o tipo T deve ser alguma classe que obrigatoriamente implemente a interface IDomainEvent, e que para o exemplo que estamos utilizando e evoluindo é a classe SaldoDaContaAlterado.

public interface IHandler<T> 
    where T : IDomainEvent
{
    void Handle(T @event);
}

Com a interface criada, temos que implementar a mesma em classes que serão consideradas os tratadores dos eventos, substituindo o tipo T por algum evento que nosso domínio define e estamos interessados em sermos notificados quando ele acontecer. Dentro da implementação do método Handle ficamos livres para executar tudo o que for necessário para aquele contexto, e que no nosso caso, é colocar “uma lupa” sobre o cliente que está com saldo negativo. Note que como parâmetro do método Handle temos (ou deveríamos ter) todas as informações necessárias a respeito do que ocorreu.

public class MonitorDeClientes : IHandler<SaldoDaContaAlterado>
{
    public void Handle(SaldoDaContaAlterado @event)
    {
        if (@event.SaldoAtual < @event.SaldoAnterior)
        {
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine(
                "Monitorando o Cliente {0}. Saldo: {1:N2}",
                @event.NomeDoCliente,
                @event.SaldoAnterior);

            Console.ResetColor();
        }
    }
}

Uma vez que a classe concreta está criada e implementada, precisamos acopla-la a execução para que ela seja executada. Agora fazemos o uso da classe DomainEvents para acomodar a relação dos eventos de domínio. Aqui temos duas formas de proceder, sendo uma lista de tratadores estáticos ou de tratadores dinâmicos. Os tratadores estáticos permitem à aplicação já identificar todos os tratadores existentes, em outras palavras, podemos utilizar Reflection para encontrar todas as classes que implementam a interface IHandler<T> e adiciona-las a coleção de tratadores da aplicação, e também via Reflection, instanciarmos essas classes que representam os eventos toda vez em que ele for disparado pela domínio.

public static class DomainEvents
{
    private static List<Type> handlers = new List<Type>();

    static DomainEvents()
    {
        handlers =
            (
                from t in Assembly.GetExecutingAssembly().GetTypes()
                from i in t.GetInterfaces()
                where
                    i.IsGenericType &&
                    i.GetGenericTypeDefinition() == typeof(IHandler<>)
                select t
            ).ToList();
    }

    public static void Raise<T>(T @event) where T : IDomainEvent
    {
        handlers.ForEach(h =>
        {
            if (typeof(IHandler<T>).IsAssignableFrom(h))
                ((IHandler<T>)Activator.CreateInstance(h)).Handle(@event);
        });
    }
}

O uso externo da classe ContaCorrente não muda em nada, ou seja, continuamos interagindo com os métodos públicos que ela expõe. Como a varredura em busca por classes que implementam a interface IHandler<T> está no construtor estático da classe DomainEvents, tão logo quando a aplicação for inicializada os tipos serão identificados e adicionado, e quando o método Raise for invocado quando um lançamento de débito ou crédito ocorrer, o nome do cliente e seu saldo serão apresentados na tela em cor vermelha.

var cc = new ContaCorrente("Israel Aece");
cc.Lancar(new ContaCorrente.Lancamento("Pagto de Energia", -1000M));

A outra opção que temos é a relação dinâmica de tratadores, onde também podemos utilizar Reflection para descobrir os tratadores que implementam a interface IHandler<T>, porém há a possibilidade de dinamicamente adicionar novos tratadores em tempo de execução de acordo com a necessidade através do método Register. O método Raise agora já não instancia dinamicamente o tratador, ou seja, isso é responsabilidade do código que o consome, dando a possibilidade de fazer uso da instância antes e depois se desejar, o que pode ser útil durante os testes para saber se o evento foi o não disparado.

public class MonitorDeClientes : IHandler<SaldoDaContaAlterado>
{
    public readonly List<string> ClientesMonitorados = new List<string>();

    public void Handle(SaldoDaContaAlterado @event)
    {
        if (@event.SaldoAtual < @event.SaldoAnterior)
            this.ClientesMonitorados.Add(@event.NomeDoCliente);
    }
}

Neste modelo, para exemplificar, ao invés de escrever na tela o cliente monitorado, o adicionamos na coleção de clientes, que nada mais é que um campo da classe. E a classe DomainEvents também mudará a sua implementação para possibilitar o vínculo dinâmico de eventos, onde temos um dicionário que para cada tipo de evento uma coleção de delegates é criada.

public static class DomainEvents
{
    private static Dictionary<Type, List<Delegate>> handlers =
        new Dictionary<Type, List<Delegate>>();

    static DomainEvents()
    {
        handlers =
            (
                from t in Assembly.GetExecutingAssembly().GetTypes()
                where
                    !t.IsInterface &&
                    typeof(IDomainEvent).IsAssignableFrom(t)
                select t
            ).ToDictionary(t => t, t => new List<Delegate>());
    }

    public static void Register<T>(Action<T> handler) where T : IDomainEvent
    {
        handlers[typeof(T)].Add(handler);
    }

    public static void Raise<T>(T @event) where T : IDomainEvent
    {
        handlers[typeof(T)].ForEach(h => ((Action<T>)h)(@event));
    }
}

Por fim, o código que consome também sofrerá uma alteração para exibir o uso monitor antes e depois do evento que foi disparado.

var monitor = new MonitorDeClientes();
DomainEvents.Register<SaldoDaContaAlterado>(monitor.Handle);

var cc = new ContaCorrente("Israel Aece");
cc.Lancar(new ContaCorrente.Lancamento("Pagto de Energia", -1000));

Console.WriteLine("Qtde: {0}", monitor.ClientesMonitorados.Count);

Em ambas as técnicas é possível ter diversos tratadores para um mesmo evento gerado. Isso é comum e muito mais elegante do que em um simples tratador realizar mais tarefas do que ele deveria fazer. Se ele é responsável por monitorar, não deveria ser responsável por notificar o gerente que a conta de seu cliente ficou negativa. Nos tratadores também vale o princípio de responsabilidade única para garantir uma fácil manutenção e legibilidade.

Para finalizar, essas técnicas funcionam bem, mas existem alguns problemas funcionais que podem tornar o sistema propício a falhas. Mas isso será assunto do próximo artigo da série.

Publicidade

Eventos de Domínio – Geração

Os eventos de domínio nos permite identificar ações importantes que ocorrem em nossa aplicação e que desejamos divulga-la para os interessados. Por interessados, leia-se outras aplicações ou, principalmente, outros contextos que estão interligados e que reagem aos eventos para realizar uma outra atividade relacionada aquela que acabou de acontecer. Para um exemplo simples, considere uma conta corrente que ao atingir o valor negativo, a central de risco do banco deve ser acionada para entender o que está havendo com o cliente e, eventualmente, monitorar as suas atividades financeiras para evitar um prejuízo maior.

O lançamento de débito ou crédito se dá na conta corrente, e se a regra de monitoramento for atendida, temos que passar a monitorar o respectivo cliente. Incorporar eventos à classe correspondente, que neste caso é a classe que representa a conta corrente, ajudará em uma centralização de código, fácil manutenção e, principalmente, agregando à ela a responsabilidade de notificar que o saldo foi alterado (para cima ou para baixo). Competirá aos consumidores a usar a informação de acordo com a sua necessidade. O monitor de risco talvez não esteja interessado em uma conta que ficou “menos negativa”.

public class ContaCorrente
{
    //Outros membros ocultados

    public void Lancar(Lancamento lancamento)
    {
        var saldoAnterior = this.Saldo;

        this.lancamentos.Add(lancamento);
        this.Saldo += lancamento.Valor;
    }
}

Por agora, tudo o que o método acima faz é alteração da propriedade que armazena o saldo e inclui um novo lançamento na coleção interna. Depois do saldo alterado, chega o momento da conta corrente gerar o evento para notificar a alteração no saldo. A implementação padrão de eventos de domínio consiste na criação de uma marker interface, que geralmente não possui nenhum membro. Ao contrário do que acontece no .NET, onde os eventos são representados por delegates, no domínio utilizamos simples classes que implementam esta interface:

public interface IDomainEvent { }

A nomenclatura destas classes são sempre definidas no passado, que indicará que algo já ocorreu, por exemplo: NovoPedidoAdicionado, NotaFiscalEmitida, e para o nosso exemplo, SaldoDaContaAlterado. Vale lembrar que a nomenclatura deve expressar, e muito, exatamente o que ocorreu. E como já era de se esperar, todas as classes que representam eventos devem implementar a interface IDomainEvent que será útil para garantirmos a construção e uso dos tipos que envolvem a infraestrutura de eventos:

public class SaldoDaContaAlterado : IDomainEvent
{
    public SaldoDaContaAlterado(
        string nomeDoCliente, decimal saldoAnterior, decimal saldoAtual)
    {
        this.NomeDoCliente = nomeDoCliente;
        this.SaldoAnterior = saldoAnterior;
        this.SaldoAtual = saldoAtual;
    }

    public string NomeDoCliente { get; private set; }

    public decimal SaldoAnterior { get; private set; }

    public decimal SaldoAtual { get; private set; }
}

É importante notarmos que a classe que representa o evento possui algumas propriedades para descrever o que ocorreu, mas sempre temos que nos atentar em o que colocar ali, tentando manter a regra do mínimo possível necessário. Seguem algumas considerações importantes que devemos ter em mente durante a construção destas classes:

  • Entidades: é tentador colocar nestas propriedades a própria entidade que sofreu a alteração (ContaCorrente). Devemos ao máximo evitar isso, pois causará uma dependência destas classes para os interessados ao evento. Muitas vezes os eventos serão utilizados para comunicação entre contextos, e isso evitará a necessidade de referenciar a entidade (física e virtualmente). Tente optar sempre por tipos primitivos.
  • Imutabilidade: essas classes não devem ter qualquer funcionalidade (métodos), apenas os dados que correspondem ao evento gerado. As propriedades são de somente leitura e são abastecidas no construtor.
  • Ids: o Id da entidade pode ser definido no evento, mas isso só faz sentido se o consumidor já estiver colaborando com o gerador e tiver condições de recarregar a entidade, ou seja, ter acesso ao mesmo repositório. Caso o acesso não seja possível, teremos que incorporar na classe todas as informações necessárias para reportar a alteração, nem que seja necessário a duplicação de todas as informações da entidade.

Uma vez que os eventos já estão definidos, chega o momento de disparar. Como mencionado acima, o responsável pelo disparo será a própria classe que identifica a mudança, e que neste caso é a ContaCorrente, e sem qualquer análise de outra condição, dispara o evento informando que o saldo foi alterado. Note que ela irá recorrer à classe estática chamada de DomainEvents.

public class ContaCorrente
{
    //Outros membros ocultados

    public void Lancar(Lancamento lancamento)
    {
        var saldoAnterior = this.Saldo;

        this.lancamentos.Add(lancamento);
        this.Saldo += lancamento.Valor;

        DomainEvents.Raise(
            new SaldoDaContaAlterado(this.NomeDoCliente, saldoAnterior, this.Saldo));
    }
}

Agora é de responsabilidade da classe DomainEvents encontrar os consumidores deste tipo de evento e notifica-los da alteração. Há diversas técnicas que podemos utilizar na implementação da classe DomainEvents bem como em seus consumidores, mas que merece um artigo específico, e será abordado na sequência desta série.