C# -  Padrão Comportamental Gof - Observer (revisitado)


Neste artigo vamos recordar o padrão comportamental Gof Observer.

Segundo a definição da Gang Of Four(GoF), o padrão Observer define uma dependência um-para-muitos entre objetos para que, quando um objeto mudar de estado, todos os seus dependentes sejam notificados e atualizados automaticamente.

Este padrão permite que um objeto notifique outros objetos sobre alterações em seu estado.

Neste padrão temos um objeto (chamado de Subject) mantém uma lista de seus dependentes (chamados de Observers) e os notifica automaticamente sempre que algum estado muda, chamando um de seus métodos.

Podemos dizer de forma simples que o objetivo principal do padrão Observer é manter a consistência entre objetos relacionados sem criar um código fortemente acoplado.



Então o padrão Observer é útil quando precisamos que dois ou mais objetos "escutem" a determinados eventos em um outro objeto.

Os objetos que estão escutando são conhecidos como Observers e o objeto que é escutado (ou observado) é conhecido como Subject.

Dessa forma podemos dizer que o padrão Observer tem dois componentes principais:

1- Subject
São objetos publicadores (publishers) – Quando ocorre alguma mudança no estado de um Subject ele deve notificar todos os seus assinantes (subscribers)

2- Observers
Eles são os assinantes (subscribers). Eles simplesmente ouvem as mudanças dos subjects. 

Uma tradução livre para Subject seria Sujeito e para Observer seria Observadores eu vou usar os nomes no idioma inglês.

Observer : Exemplo

Vejamos um exemplo para entender o padrão Observer.

Suponha que temos um Subject (que tem um estado) e alguns Observers:



Neste cenário,  os objetos Observers querem ser atualizados quando algo mudar no Subject.

Então para isso a  cada 10s eles enviam uma solicitação ao Subject se algo mudou, e, assim o objeto Subject tem que ser conhecido pelos objetos Observers.



Temos aqui que muitos Observers dependem de um Subject (definindo o relacionamento de muitos para um).

Percebemos que o problema aqui é manter os objetos Observers atualizados com relação as mudanças que o objeto Subject publica.

Agora vamos supor que o intervalo entre as mudanças seja de 60 segundos.  Neste intervalo cada objeto Observer já realizou 6 solicitações ao objeto Subject sendo que somente a última retornará com uma resposta positiva e as demais foram desnecessárias.

Outro problema aqui é a quantidade de objetos Observers quando mais objetos Observers existirem maior será a quantidade
de solicitações não necessárias. Assim sem usar o padrão Observer os observadores tem que ficar enviando solicitações ao subject em intervalos de tempo o que pode consumir recursos e não ser eficiente.;

Para resolver este problema podemos usar o padrão Observer.



A proposta do padrão Observer é que os objetos Observers devem pedir ao Subject que os notifiquem quando algo mudar.

Assim teremos o objeto Subject e os objetos Observers mas agora o objeto Subject é quem notifica os observers quando houver alguma mudança.  E assim agora temos que  um Subject depende de muitos Obsevers (dependência um-para-muitos)

Mas para isso funcionar é preciso que haja um acordo entre o Subject e os Observers. Onde os Observers  :

- Precisam fornecer um canal para que o Subject os notifiquem.
- Precisam conhecer o Subject que desejam observar

E o Subject :

- Precisa fornecer um canal para que os observers possam se cadastrar como observadores.
- E também um canal para que os Observers possam se descadastrar como observadores.
- Deve ser capaz de notificar todos os observers quando sofrer uma atualização

Para isso o Subject deve manter uma lista dos Observers que se inscreveram para serem notificados das mudanças. E agora sempre que ocorrer alguma mudança no seu estado o Subject envia uma notificação para todos os inscritos na lista. Com isso não existem mais o problema das solicitações desnecessárias.

Com isso o uso do padrão Observer fornece uma forma de reagir a eventos que acontecem em objetos sem ter um forte acoplamento em suas classes.

A ideia fundamental por trás do padrão do observador é centralizar a tarefa de informar dentro do Subject.  Portanto, ele mantém uma lista na qual os observadores podem se cadastrar para ingressar. Em caso de alteração, o subject informa os observadores cadastrados - um após o outro - sem que eles próprios tenham que agir.

Se uma atualização automática de status não for mais desejada para um determinado objeto observador, ela pode simplesmente ser removida da lista.

Diagrama UML

O diagrama UML do padrão Iterator segundo o Gof apresenta os seguintes participantes

1- Subject -

- Conhece seus Observers;
- Fornece uma interface para anexar e desanexar objetos Observer;
- Qualquer número de objetos Observer pode observar um Subject;

2- ConcreteSubject

- Armazena o estado de interesse para ConcreteObserver;
- Envia uma notificação para seus observadores quando seu estado muda;

3- Observer - Def

- Define uma interface de atualização para objetos que devem ser notificados das mudanças em um Subject;

4- ConcreteObserver -  I

- Mantém uma referência a um objeto ConcreteSubject;
-Implementa a interface de atualização do Observer para manter seu estado consistente com o do Subject;
- Armazena o estado que deve permanecer consistente com o do Subject;

Quando podemos usar o padrão

Podemos usar o padrão Iterator nos seguintes cenários :

- Quando uma modificação do estado de um objeto implica modificações em outros objetos;
- Quando um objeto deve ser capaz de notificar outros objetos, mas sem pressupostos sobre os objetos a serem notificados;
- Quando uma abstração possuir dois aspectos e um depende do outro;
- Quando não desejamos um forte acoplamento com os objetos que necessitam conhecer estas modificações;


Vantagens do padrão

Como vantagens deste padrão temos que :

-Permite um acoplamento mínimo entre o Subject e o Observer;
-Pode reutilizar Subjects sem reutilizar os seus Observers e vice-versa;
-Os Observers podem ser adicionados sem modificar o Subject;
-O Subject e Observer podem pertencer a diferentes camadas de abstração;
-O Subjeito e Observador podem pertencer a diferentes camadas de abstração;

Desvantagem

E como desvantagem podemos citar que :

Os assinantes são notificados em ordem aleatória. Também pode ocorrer o problema de vazamento de memória devido ao registro explícito e ao cancelamento do registro do Observer.

Aplicação prática do padrão

Como exemplo de aplicação do padrão vamos imaginar o seguinte cenário:

1- 3 usuários visitam um site de vendas on line para comprar o mesmo celular - um IPhone 11 :



Acontece que no momento este celular não esta disponível ou seja a loja esta sem estoque e assim não é possível comprar o produto no momento.

Para estes casos o site oferece uma opção para notificar o usuário quando o produto estiver disponível. Para isso os usuários tem que se registrar no serviço marcando a opção para serem notificados quando o celular chegar.  Assim quando o celular IPhone 11 chegar os usuários receberam uma notificação do site.

Então após algum tempo a loja do site recebeu o produto e o seus status passou de - Sem Estoque - para - Disponível - e com isso o site enviou notificações para todos os usuários que se registraram no serviço.

Aqui podemos identificar os dois componentes principais do padrão Observer :

- O Celular IPhone 11 representa o Subject;
- Os usuários são os Observers;


E assim de acordo com o padrão Observer, os Observers precisam estar registrados para o Subject.  No nosso caso, os três usuários estão cadastrados para a opção de notificação do Subject.  Quando o estado do Subjcet mudar de - Sem Estoque - para Disponível - o Subject enviará uma notificação a todos os observers.

Temos aqui um exemplo de atuação do padrão observer a qual iremos implementar via código usando o C#.

Implementação prática

Levando em conta este cenário vamos implementar o padrão Observer usando uma aplicação Console .NET Core (.NET 5.0) criada no VS 2019 Community.

A seguir temos o diagrama de classes obtido a partir do VS 2019 na implementação do padrão:

1- ISubject - Define o contrato para registrar, remover e notificar Observers;

2 -ConcreteSubject - Define uma lista de observers e o produto cujo estado será observado e cria um objeto produto e define seu status inicial (subject);

- define o método para retornar a disponibilidade atual do produto;
- Altera o status do produto definindo sua disponibilidade atual;
- Registrar um observer interessado em recebe notificações sobre atualização do status de produto;
- Define um método para incluir o observer na lista de observadores;
- Define um método para remover o observer da lista de observadores;
- Notifica os observers cadastrados na lista de observadores;

3- IObserver -  Define o contrato para atualizar os objetos que devem ser notificados das mudanças no status do produto

4- ConcreteObserver - Implementa a interface de atualização do Observer e mantêm referencia ao objeto ConcreteSubject;

A seguir temos o código usado na implementação:

1- A interface ISubject

public interface ISubject
{
    void RegistrarObserver(IObserver observer);
    void RemoverObserver(IObserver observer);
    void NotificarObservers();
}

2- A Interface IObserver

public interface IObserver
{
   void Atualiza(string disponibilidade);
}

3- Classe ConcreteSubject

using System;
using System.Collections.Generic;
namespace ObserverExemplo
{
    public class ConcreteSubject : ISubject
    {
        private List<IObserver> observers = new List<IObserver>();
        private string Produto { get; set; }
        private int Preco { get; set; }
        private string Disponibilidade { get; set; }
        public ConcreteSubject(string produto, int preco, string status)
        {
            Produto = produto;
            Preco = preco;
            Disponibilidade = status;
        }
        public string GetDisponibilidade()
        {
            return Disponibilidade;
        }
        public void SetDisponibilidade(string status)
        {
            this.Disponibilidade = status;
            Console.WriteLine("A disponibilidade mudou de 'Sem Estoque' para 'Disponível'.");
            NotificarObservers();
        }
        public void RegistrarObserver(IObserver observer)
        {
            Console.WriteLine("Observer Adicionado : " + ((ConcreteObserver)observer).Usuario);
            observers.Add(observer);
        }
        public void AdicionarObservers(IObserver observer)
        {
            observers.Add(observer);
        }
        public void RemoverObserver(IObserver observer)
        {
            observers.Remove(observer);
        }
        public void NotificarObservers()
        {
            Console.WriteLine($"O Produto :{Produto} no valor de R$ {Preco} agora esta disponível." + 
                "\n### Notificando todos os Observers registrados ### ");
            Console.WriteLine();
            
            foreach (IObserver observer in observers)
            {
                observer.Atualiza(Disponibilidade);
            }
        }
    }
}

4- Classe ConcreteObserver

using System;
namespace ObserverExemplo
{
    public class ConcreteObserver : IObserver
    {
        public string Usuario { get; set; }
        public ConcreteObserver(string nome, ISubject subject)
        {
            Usuario = nome;
            subject.RegistrarObserver(this);
        }
        public void Atualiza(string disponibilidade)
        {
            Console.WriteLine($"Olá {Usuario}, o Produto que você deseja agora " +
                $"esta {disponibilidade} em nosso site");
        }
    }
}

7- Program

using System;

namespace ObserverExemplo
{
    class Program
    {
        static void Main(string[] args)
        {
            //cria um produto sem estoque e exibe disponibilidade
            ConcreteSubject IPhone11 = new ConcreteSubject("Phone 11 ", 4900, "Sem Estoque");
            Console.WriteLine("Phone 11 - estado atual : " + IPhone11.GetDisponibilidade());

            Console.WriteLine("\nObservers inscritos para receber notificações sobre " +
                "o produto IPhone 11\n");

            //cria Usuario Macoratti e registra objeto no Subject
            ConcreteObserver macoratti = new ConcreteObserver("Macoratti", IPhone11);
            //cria Usuario Miriam e registra objeto no Subject
            ConcreteObserver miriam = new ConcreteObserver("Miriam", IPhone11);
            //cria Usuario Janice e registra objeto user1 no Subject
            ConcreteObserver janice = new ConcreteObserver("Janice", IPhone11);

            Console.WriteLine("\nPressione algo para alterar a disponibilidade e " +
                "notificar os observers\n");
            Console.ReadKey();

            //altera status do Subject e notifica observers
            IPhone11.SetDisponibilidade("Disponível");

            Console.Read();
        }
    }
}

A execução do projeto irá apresentar o seguinte resultado:

O padrão Observer talvez seja um dos padrões de projetos mais usados, principalmente para aplicações web. Conhecer e saber aplicar os conceitos envolvidos neste padrão deve ser um quesito imprescindível para todo o desenvolvedor.

Pegue o código do projeto aqui :   ObserverExemplo.zip

"Porque, se alguém cuida ser alguma coisa, não sendo nada, engana-se a si mesmo. Mas prove cada um a sua própria obra, e terá glória só em si mesmo, e não noutro."
Gálatas 6:3,4

Referências:


José Carlos Macoratti