Neste artigo, veremos na prática a implementação de um notificador de eventos (o famoso padrão “Observer”) fazendo uso de um container de DI. A solução proposta é flexível, utilizando boas práticas de design e arquitetura de software.
____________________________
Nota: este é um artigo avançado, pois utiliza-se de padrões como Observer e Ambient Context, conceitos como Dependency Injection, CompositionRoot e Princípio da Inversão de Dependência e práticas de teste como mocks. Portanto, esses conceitos são necessários para um melhor entendimento do “porquê” das coisas. O artigo fornece diversas referências para todos eles.
____________________________

NOTIFICANDO EVENTOS

Um notificador de eventos é uma variação do conhecido design pattern Publish/Subscribe (ou Observer). Seguindo a definição do padrão à risca, o “publisher” (nosso Notificador) notifica todos os interessados (os “observers”) sempre que aquele muda seu estado.

É uma boa solução quando temos uma relação de um para muitos entre objetos e não queremos acoplá-los por se tratarem de objetos de outras camadas e para deixar o notificador mais flexível em relação aos seus “observers”.

Obs. 1: No nosso caso, o notificador não notificará mudança de estado – veremos que ele nem sequer possui estado – e sim um evento ocorrido em nossa aplicação.

Obs. 2: O evento, no caso deste artigo, será um evento de domínio, ou seja, uma situação ocorrida em nossa lógica de negócio e que precisa ser comunicada a outros contextos (que podem ser outras aplicações). No entanto, a mesma ideia mostrada no artigo pode ser utilizada para notificar outros tipos de eventos (de outras camadas).

IMPLEMENTANDO UM NOTIFICADOR BASEADO EM UM CONTAINER DE DI

Vendo a definição formal do padrão Observer no link mais acima, vocês podem notar que o “publisher/subject” expõe métodos para adicionar e remover os “observers” que deverão ser notificados. Em outras palavras, o notificador mantém a lista de “observers”.

No exemplo deste artigo, a implementação adotada faz uso de um container de Dependency Injection para localizar seus “observers” e poder notificá-los, dispensando assim que o notificador conheça a lista de seus “observers”.

Vejamos como fica o código:

// o "publisher/subject"
public class NotificadorDeEventosDeDominio
{
    public static void NotificarSobre<T>(T eventoDeDominio) where T : IEventoDeDominio
    {
         foreach (var enderecador in this.container.GetAllInstances<IEnderecadorDeEvento<T>>())
             enderecador.Enderecar(eventoDeDominio);
    }
}

// interface de marcação para os eventos de domínio
public interface IEventoDeDominio { }

// o "subscriber/observer"
public interface IEnderecadorDeEvento<T> where T : IEventoDeDominio
{
    void Enderecar(T eventoDeDominio);
}

Algumas observações sobre o código:

  • O notificador não conhece seus “observers”. Ao invés disso, ele pede ao container de DI para localizá-los, ou seja, estamos usando o container como um Service Locator. Muita cautela com esse tipo de prática. Não espalhe o container por toda sua aplicação, pois além da dependência concreta em relação ao container, você estará escondendo todas as dependências que suas classes possuem.
  • O container de DI é injetado via construtor e armazenado em um field usado no método acima (this.container).
  • Qualquer container de DI de sua preferência pode ser utilizado.
  • IEventoDeDominio é apenas uma interface de marcação para restringirmos o parâmetro do método e os “observers” para aceitarem somente objetos que sejam eventos de domínio.
  • Tanto o notificador como as interfaces usadas estão na camada de domínio, pois estão diretamente relacionados.

Um exemplo de uso seria o seguinte:

public class Despesa
{
    public void Baixar()
    {
        // baixa da despesa ....

        // notificando evento
        NotificadorDeEventosDeDominio
            .NotificarSobre(new DespesaPaga(idDespesa, valorPago));
    }
}

No exemplo acima, uma entidade do domínio, após realizar sua operação de baixa, notifica aos interessados a ocorrência do evento DespesaPaga. Um “observer” que receberia essa notificação seria qualquer objeto que implemente IEnderecadorDeDominio<DespesaPaga>.

 ALGUNS PROBLEMAS

A solução mostrada apresenta alguns problemas:

1. Como dito antes, o NotificadorDeEventosDeDominio encontra-se na camada de domínio, juntamente com a classe Despesa e demais classes que modelam o negócio. Como usamos o container de DI na implementação do notificador, somos obrigados a manter uma referência do container no domínio, ou seja, o domínio conhece um detalhe técnico (o container).

De um ponto de vista arquitetural, devemos deixar o domínio o mais ignorante possível de detalhes técnicos (mais aqui).

Portanto, precisamos remover essa dependência do domínio para o container de DI!

2. Testabilidade! Conseguimos garantir que o estado do objeto Despesa foi alterado na baixa mas não conseguimos testar que a notificação realmente foi chamada, uma vez que fazemos isso por meio de um método estático do notificador.

VERSÃO MELHORADA

Para sanar os problemas acima, precisamos abstrair o notificador de eventos, aplicando o Princípio da Inversão de Dependência.

No entanto, quero continuar usando o notificador como um ambient context, ou seja, não quero poluir a interface das minhas entidades de domínio injetando o notificador via construtor, método ou propriedade.

Vejamos como fica a nova versão, começando pela abstração:

public abstract class NotificadorDeEventosDeDominio
{
    private static NotificadorDeEventosDeDominio _notificadorCorrente;

    public static NotificadorDeEventosDeDominio NotificadorCorrente
    {
        get { return _notificadorCorrente; }
        set
        {
            if (value == null)
               throw new ArgumentNullException("value");

            _notificadorCorrente = value;
        }
    }

    public abstract void NotificarSobre<T>(T eventoDeDominio) where T : IEventoDeDominio;
}

O notificador concreto é praticamente o anterior, porém herdando da abstração acima:

public class NotificadorBaseadoEmContainerDeDi : NotificadorDeEventosDeDominio
{
    // construtor com injecao do container omitido

    public override void NotificarSobre<T>(T eventoDeDominio)
    {
        foreach (var enderecador in this.container.GetAllInstances<IEnderecadorDeEvento<T>>())
           enderecador.Enderecar(eventoDeDominio);
    }
}

Agora, os objetos de domínio passam a depender da abstração e não mais do notificador concreto:

public class Despesa
{
    public void Baixar()
    {
        // baixa da despesa ...

        // notificando evento
        NotificadorDeEventosDeDominio
             .NotificadorCorrente.NotificarSobre(new DespesaPaga(idDespesa, valorPago));
    }
}

Sendo assim, podemos mover o notificador concreto do domínio para o assembly que faz uso do container de DI para subir a aplicação. Com isso, o domínio passa a não conhecer mais o container de DI.

GARANTINDO A TESTABILIDADE

Com a nova versão, podemos testar melhor os objetos de domínio que fazem uso do notificador.

Vejamos como fica o teste da entidade Despesa, que verifica se o notificador foi realmente chamado (e com o evento correto):

[Test]
public void Deve_notificar_sobre_baixa_da_despesa()
{
    var notificadorDeEventosDeDominioMock = new Mock<NotificadorDeEventosDeDominio>();
    NotificadorDeEventosDeDominio.NotificadorCorrente = notificadorDeEventosDeDominioMock.Object;

    var despesa = new Despesa();

    despesa.Baixar();

    var despesaPaga = (new DespesaPaga(100, 100.50)).AsSource().OfLikeness<DespesaPaga>().CreateProxy();
    notificadorDeEventosDeDominioMock.Verify(mock => mock.NotificarSobre(despesaPaga), Times.Once);
}

Vejam que, para fins de teste, configuramos o NotificadorCorrente com um mock ao invés de usarmos o notificador concreto e fazemos a asserção do teste verificando que o método NotificarSobre() foi realmente chamado com o evento correto e somente uma vez.

(Obs.: Neste teste, utilizei a lib SemanticComparison para criar o objeto esperado como parâmetro do método. Para saber mais sobre ela, leia meu post anterior sobre comparação semântica.)

USANDO O NOTIFICADOR CONCRETO

Ok, usamos um mock como NotificadorCorrente nos testes de unidade. E no código de produção? Onde usamos o notificador concreto?

A resposta é simples: configuramos o NotificadorCorrente com o notificador concreto no start da aplicação (*) e nunca mais mexemos com ele:

var container = ConfigurarContainer();
NotificadorDeEventosDeDominio.NotificadorCorrente =
           new NotificadorBaseadoEmContainerDeDi(container);

(*) Esse local é chamado de CompositionRoot e falei sobre ele na minha série sobre Dependency Injection.

Relembrando que o notificador concreto fica junto do container no mesmo assembly, que pode ser o assembly referente à camada de apresentação ou um específico para o CompositionRoot (o que eu acho mais adequado em projetos reais).

E OS OBSERVERS CONCRETOS?

Os observers concretos – aqueles que implementam IEnderecadorDeEvento<T> – ficam na camada correspondente ao seu propósito. Se o observer, por exemplo, informa alguma coisa para a view ele ficará na camada de apresentação. Outro observer que trata o evento comunicando-se com algum serviço externo (e-mail, API REST, messaging) certamente ficará na camada de infra-estrutura. E assim por diante.

CONCLUSÃO

Vimos, passo-a-passo, como criar um notificador de eventos (padrão Observer) usando um container de DI em sua implementação.

A solução mostrada apresenta baixo acoplamento entre os módulos, o que garante testabilidade dos clientes do notificador e atende a regra de dependências entre camadas (sempre da mais externa para a mais interna).

Foi um artigo avançado, que exigiu conhecimento de diversos padrões de design e arquitetura de software, mas espero ter conseguido passar a ideia.

Para reforçar o entendimento, disponibilizei no meu github o código do artigo juntamente com um pequeno exemplo funcional de uma app console, que faz uso de tudo que foi visto aqui.