Inversão de controle Parte 3: Observers

Criança olhando por uma luneta

Olá! Tudo bem? Este é o terceiro artigo de uma série sobre inversão de controle. No primeiro artigo, abordamos aspectos conceituais deste princípio. No segundo, discutimos sobre como o padrão factory pode nos ajudar na sua implementação. Neste artigo, vamos entender o design pattern observer e porquê ele faz parte desta lista de artigos sobre IoC.

Por que Observers?

Quando tentamos aplicar todas as boas práticas de codificação, inevitavelmente vamos ter um número grande de classes cooperando entre si. Esta cooperação leva a uma dependência intrínseca entre as classes, levando ao indesejável acoplamento.

Vamos supor, por exemplo, que você construiu um software que está responsável ler o estado de um objeto através de uma API qualquer. Quando alguma mudança de estado acontecer, você precisa atualizar os dados em tela. Do contrário, nada acontece. Para escrever esse código, sem utilizar observers, a classe responsável por verificar a API deveria conhecer a classe responsável por mostrar os dados em tela. Isso, de imediato, geraria uma dependência.

Mas o requisito mudou. E agora, além de mostrar as informações em tela, você terá que enviar uma notificação push para o usuário. E agora? Vamos aumentar mais o acoplamento entre as classes? E como os requisitos não param de mudar, ao invés de notificar uma classe, você precisa notificar outro (ou outros) sistemas que o objeto sofreu modificações. E agora?

O padrão Observer vem justamente atender a esses casos. Com ele, a classe principal pode apenas notificar que algo aconteceu e todas as dependentes podem reagir a este acontecimento. Isto sem que elas se conheçam.

Mas como isso funciona?

Não é possível entrar em detalhes profundos sobre como implementar este padrão. Do contrário, teríamos um capítulo e não um artigo. Mas vou tentar explicar pra você a forma mais simples de construir um observer.

Passo1: Crie a interface observer.

Esta interface terá apenas o método Update. Este método será chamado toda vez que a mensagem deve ser transmitida. Continuando com o exemplo que citamos antes, o método Update será chamado todas as vezes que uma alteração de estado acontecer no objeto da API. As classes que cooperam devem, portanto, implementar esta interface.

namespace Observer
{
    interface IObserver
    {
        void Update(ObjetoDTO objeto);
    }
}

Talvez você precise de informações para executar as ações dependentes, como por exemplo, para qual estado o objeto se modificou? Assim é ser uma boa ideia que o método update receba informações sobre o objeto observado.

using System;

namespace Observer
{
    class NotificadorConsole : IObserver
    {
        public void Update(ObjetoDTO objeto)
        {
            Console.WriteLine($"Notificando no console: Nome {objeto.Nome}; Estado: {objeto.Estado}");
        }
    }
}

Passo2: Crie uma interface que atue como subject.

Esta interface deve conter basicamente três métodos: Adicionar, Remover e Notificar. Os métodos Adicionar e Remover recebem, como parâmetro, instâncias da interface observer que criamos no passo 1. O método Notificar deve receber, como parâmetro, o mesmo tipo que a interface observer.

namespace Observer
{
    interface ISubject
    {
        void Adicionar(IObserver observer);
        void Remover(IObserver observer);
        void Notificar(ObjetoDTO objeto);
    }
}

Desta forma, a classe que implementar a interface subject terá que se preocupar em armazenar a lista de observers. Quando receber notificações, por meio do método Notificar, a classe irá iterar sobre a lista de observers. Para cada item da iteração, será chamado o método Update, passando como parâmetro o mesmo objeto recebido pelo método Notificar.

using System.Collections.Generic;

namespace Observer
{
    class Subject : ISubject
    {
        public Subject()
        {
            _observers = new List<IObserver>();
        }
        public void Adicionar(IObserver observer)
        {
            _observers.Add(observer);
        }

        public void Notificar(ObjetoDTO objeto)
        {
            foreach (var observer in _observers)
            {
                observer.Update(objeto);
            };
        }

        public void Remover(IObserver observer)
        {
            _observers.Remove(observer);
        }

        private List<IObserver> _observers;
    }
}

Passo 3: Utilizando o observer

Existem várias formas de utilizar o observer. Através de um Mediator, onde ele manipula as classes e emite a notificação. Também pode injetar o subject nas classe cliente como dependência. Você teria, como dependência, apenas uma interface e não uma multidão de classes. E o melhor: nenhuma das pontas precisa conhecer.

No exemplo a seguir, nós criamos duas instâncias de IObserver (NotificadorConsole e NotificadorBlueTooth) e a adicionamos à instância de subject que também criamos. A classe Cliente recebe uma instância do subject no construtor. Internamente, quando necessário, a classe Cliente dispara a notificação e o subject notifica as instâncias anexadas a ele.

using System;

namespace Observer
{
    class Program
    {
        static void Main(string[] args)
        {
            var notificadorConsole = new NotificadorConsole();
            var notificadorBlueTooth = new NotificadorBlueTooth();

            var subject = new Subject();
            subject.Adicionar(notificadorConsole);
            subject.Adicionar(notificadorBlueTooth);

            var cliente = new Cliente(subject);
            cliente.Executar();

            Console.WriteLine();
            Console.WriteLine("Vamos executar sem o bluetooth");
            Console.WriteLine();
            subject.Remover(notificadorBlueTooth);
            cliente.Executar();

            Console.ReadLine();
        }
    }
}
using System;

namespace Observer
{
    class Cliente
    {
        public Cliente(ISubject subject)
        {
            _subject = subject;
        }

        public void Executar()
        {
            Console.WriteLine("Informe o nome:");
            var nome = Console.ReadLine();

            Console.WriteLine("Informe o número para o estado:");
            var stringEstado = Console.ReadLine();
            if (!int.TryParse(stringEstado, out var estado))
                Console.Write("Você não digitou um número!");

            var objeto = new ObjetoDTO
            {
                Nome = nome,
                Estado = estado
            };
            _subject.Notificar(objeto);
        }
        private ISubject _subject;
    }
}

Onde está a IoC aqui?

A principal função do padrão observer é distribuir o fluxo do sistema. Como as classes não se conhecem, não há um controle direto do fluxo. A classe que dispara a notificação sequer conhece quantas outras estão observando as suas modificações.

O mesmo acontece com aplicações distribuídas. Os sistemas que implementam o padrão pub-sub permitiram escalabilidade e independência entre as aplicações. DDD possui uma dependência implícita do padrão Observer. Sem ele, é quase impossível fazer com que os diversos domínios se comuniquem.

Como já tenho dito ao longo desses artigos, inversão de controle é justamente inverter o controle do fluxo da aplicação. O padrão observer, como vimos, toma para si o controle do fluxo, orquestrando as comunicações entre as classes e os diversos processos. Dependendo da implementação, você ainda pode juntar observers e threads, tornando essa comunicação ainda mais rápida.

Uma última palavra sobre os padrões de projeto

Antes de concluir, preciso trazer uma afirmação que pode acalentar a mente de vocês. Os padrões de projetos não precisam ser implementados ipsis literis iguais ao livro. Adaptações, quando bem feitas, são efetivas e bem vindas. O que você precisa ter realmente em mente é o tipo de problema que o padrão busca resolver e a mecânica que ele implementa. Se você não tiver muita experiência em desenvolvimento com os padrões de projeto, apenas tente não se afastar muito das ideias originais.

Por hoje é só. E no próximo artigo sim, veremos como implementar o padrão Service Locator.

Outros artigos da série

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.