Os padrões de projeto comportamentais têm por benefício a reutilização de códigos, permitindo que os desenvolvedores concentrem esforços no desenvolvimento do negócio e não na linguagem de programação que estão utilizando, ajudando assim a minimizar os erros de reduzir os prazos de entrega.

Existem três tipos básicos de padrões: criacionais, comportamentais e estruturais que, respectivamente, tratam da criação, interação e organização das interações e divisões de responsabilidades entre as classes ou objetos.

Existem diversos designer patterns conhecidos e implementados pelos arquitetos e desenvolvedores mais experientes e, pensando nisso, o framework .NET fornece algumas classes que podem ser utilizadas para a implementação destes patterns. Neste artigo iremos aprender sobre o pattern Observer, mostrando algumas formas de implementação, incluindo delegates, interfaces e algumas classes que o framework .NET nos fornece.

O pattern observer define a dependência de um para muitos (1 para n) entre objetos para que, quando um objeto mude de estados, todos os seus dependentes sejam avisados e atualizados automaticamente. Além disso, podemos usar quando uma abstração tiver dois aspectos que dependem um do outro, assim, quando precisarmos usá-los, podemos fazer de forma independente.

O Projeto

Imagine uma situação em que precisamos comunicar algo a um grupo de objetos sobre um acontecimento: o padrão observer ajuda a implementar uma solução eficaz para este problema.

Vamos pensar na seguinte situação: A empresa Xpto resolveu criar um sistema de e-mail marketing sobre promoções do site. Os analistas da empresa discutiram algum tempo e chegaram nos seguintes requisitos:

  1. Somente os usuários que se cadastraram no newsletter do site serão avisados das promoções;
  2. O usuário pode cancelar o envio das promoções quando quiser;
  3. Alguns grupos de usuário podem ser tratados de forma diferenciada.

Para este problema temos várias soluções diferentes e elas são: usando o modelo tradicional, usando delegates e usando as classes do .NET framework. Veremos as três soluções a seguir.

Modo comum de implementar o padrão Observer

Primeiro vamos criar a interface ISubject que contém os métodos de registro, remoção do usuário e o método que envia os e-mails para os usuários registrados, como mostra a Listagem 1. Para isso, abra o Visual Studio e em File > New > Project (ou clique Ctrl+Shift+N) escolha o template Console Application.

Listagem 1. Interface ISubject

public interface ISubject   
{
          void Registrar(IObserver observer);
          void Remover(IObserver observer);
          void EnviarEmail();
}

Também temos a interface IObserver que contém o método de envio dos e-mails, conforme demonstrado na Listagem 2.

Listagem 2. Interface IObserver

public interface IObserver
{
   void ReceberEmail();
}

Por fim, vamos implementar as interfaces citadas conforme mostra a Listagem 3.

Listagem 3. Implementando as interfaces das Listagens 1 e 2

public class ControladorEmail : ISubject
{
    private readonly List<IObserver> _usuarios;
  
       public ControladorEmail()
       {
           _usuarios = new List<IObserver>();
       }
   
       public void Registrar(IObserver observer)
       {
           _usuarios.Add(observer);
       }
   
       public void Remover(IObserver observer)
       {
           _usuarios.Remove(observer);
       }
   
       public void EnviarEmail()
       {
           foreach (var usuario in _usuarios)
           {
               usuario.ReceberEmail();
           }
    }
}

Note que a classe ControladorEmail herda de ISubject e contém uma lista de IObserver. Além disso, no método de registro apenas inserimos um observer na lista, e para remover basta remover o objeto observer da lista. Por fim, para notificar os usuários basta executar o método “Receberemail” para todos os observers contidos na lista.

Agora iremos implementar os usuários que serão notificados pela classe Subject, como mostra a Listagem 4.

Listagem 4. Classe Subject

public class UsuarioA : IObserver
      {
          public void ReceberEmail()
          {
              Console.WriteLine("Email Recebido pelo usuário A");
          }
      }
     
  public class UsuarioB : IObserver
      {
          public void ReceberEmail()
          {
              Console.WriteLine("Email Recebido pelo usuario B");
          }
      }
   
  public class UsuarioC : IObserver
      {
          public void ReceberEmail()
          {
              Console.WriteLine("Email Recebido pelo usuario C");
          }
}

Note que cada usuário implementa a interface IObserver de forma diferente, porém, como estamos trabalhando com interfaces, a classe ISubject executará cada observer de forma diferenciada.

Para o método main temos o código da Listagem 5.

Listagem 5. Método Main

class Program
{
   static void Main(string[] args)
   {
       ISubject controladorEmail = new ControladorEmail();
             
       var usuarioA = new UsuarioA();
       var usuarioB = new UsuarioB();
       var usuarioC = new UsuarioC();
   
       controladorEmail.Registrar(usuarioA);
       controladorEmail.Registrar(usuarioB);
       controladorEmail.Registrar(usuarioC);
   
       Console.WriteLine("Os usuarios A, B e C cadastraram-se para receber as promoções. \n");
       Console.WriteLine("Enviando os emails para os usuarios assinados (usuários cadastrados).\n");
   
       controladorEmail.EnviarEmail();
   
       Console.WriteLine("\nO usuário A resolveu concancelar a assinatura e não irá receber mais emails.\n");
       controladorEmail.Remover(usuarioA);
   
       Console.WriteLine("Enviando os emails para os usuarios assinados.\n");
       controladorEmail.EnviarEmail();
   
       Console.ReadKey();
              
       Console.ReadKey();
  }
}

Podemos ver a execução do projeto na Figura 1.

Execução do exemplo 1

Figura 1. Execução do exemplo 1

Usando Delegates

Podemos ter o mesmo recurso de avisar as classes assinantes utilizando delegates. O procedimento é parecido com o que fizemos anteriormente e a principal mudança é a forma de assinatura do delegate.

Crie um novo projeto do tipo Console Application e nele vamos criar a classe ControladorEmail, que irá delegar o Action ProcessarEmail para a classe que assinar o delegate ProcessarEmail, como mostra a Listagem 6.

Listagem 6. Classe ControladorEmail

public class ControladorEmail
{
      public Action ProcessarEmail;
   
      public void EnviarEmail()
      {
          ProcessarEmail();
      }
}

Agora iremos construir as classes que irão receber o e-mail, como mostra a Listagem 7. Logo, irão assinar o delegate da classe ControladorEmail.

Listagem 7. Classes dos tipos de usuários que receberam os e-mails

public class UsuarioA
{
          public void Assinar(ControladorEmail controlador)
          {
              controlador.ProcessarEmail += ProcessarEmail;
          }
          public void ProcessarEmail()
          {
              Console.WriteLine("Email Recebido pelo usuário A");
          }
   
          public void CancelarAssinatura(ControladorEmail controlador)
          {
              controlador.ProcessarEmail -= ProcessarEmail;
          }
}
public class UsuarioB
{
          public void Assinar(ControladorEmail controlador)
          {
              controlador.ProcessarEmail += ProcessarEmail;
          }
          public void ProcessarEmail()
          {
              Console.WriteLine("Email Recebido pelo usuário B");
          }
          public void CancelarAssinatura(ControladorEmail controlador)
          {
              controlador.ProcessarEmail -= ProcessarEmail;
          }
}
   
public class UsuarioC
{
          public void Assinar(ControladorEmail controlador)
          {
              controlador.ProcessarEmail += ProcessarEmail;
          }
          public void ProcessarEmail()
          {
              Console.WriteLine("Email Recebido pelo usuário C");
          }
          public void CancelarAssinatura(ControladorEmail controlador)
          {
              controlador.ProcessarEmail -= ProcessarEmail;
          }
}

Para o método Main deste programa usaremos o código da Listagem 8.

Listagem 8. Método Main do exemplo 2.

static void Main(string[] args)
  {
   
      ControladorEmail controlador = new ControladorEmail();
      var usuarioA = new UsuarioA();
      var usuarioB = new UsuarioB();
      var usuarioC = new UsuarioC();
   
      Console.WriteLine("Os usuarios A, B e C cadastraram-se para receber as promoções. \n");
         usuarioA.Assinar(controlador);            
       usuarioB.Assinar(controlador);            
       usuarioC.Assinar(controlador);
   
       Console.WriteLine("Enviando os emails para os usuarios assinados (usuários cadastrados).\n");
         controlador.EnviarEmail();
   
       Console.WriteLine("\nO usuário A resolveu concancelar a assinatura e não irá receber mais emails.\n");
       usuarioA.CancelarAssinatura(controlador);
   
   
       Console.WriteLine("Enviando os emails para os usuarios assinados.\n");
       controlador.EnviarEmail();
   
       Console.ReadKey();
   
   }

O resultado desse exemplo pode ser visto na Figura 2.

Execução do exemplo 2

Figura 2. Execução do exemplo 2

Usando as interfaces do .net Framework

Pensando neste padrão, o framework .NET disponibiliza duas interfaces quefornecem um mecanismo generalizado para notificação baseada em envio,que são:


  1. IObservable<T>, que é o provedor para notificação de envio;
  2. IObserver<T>, que fornece um mecanismo para receber as notificações de envio.

Para explicar o uso destas interfaces vamos realizar o mesmo exemplo feito anteriormente. Primeiramente, vamos criar uma classe que herda de IObservable<T>, sendo que, para o nosso exemplo, T será um email, mas poderia ser uma string também.

Definimos a classe Email como:

      public class Email
      {
          public string Descricao { get; set; }
}

Assim, a classe ControladorEmail ficará como mostra o código da Listagem 9.

Listagem 9. Classe ControladorEmail

      // Poderiamos apenas herdar de IObservable<string> tambem.
      public class ControladorEmail : IObservable<Email>
      {
          public List<IObserver<Email>> _usuarios;
          public Email _email;
   
          public ControladorEmail(Email email)
          {
              _usuarios = new List<IObserver<Email>>();
              _email = email;
          }
   
          public IDisposable Subscribe(IObserver<Email> usuario)
          {
              if (!_usuarios.Contains(usuario))
                  _usuarios.Add(usuario);
   
              return new Disposer(_usuarios, usuario);
          }
   
          public void EnviarEmail()
          {
              _email.Descricao = "Email Enviado para o usuário";
   
              foreach (IObserver<Email> usuario in _usuarios)
              {
                  usuario.OnNext(_email);
              }
          }
  }

Note que esta classe contém uma lista de IObserver, que poderá ser usada no método EnviarEmail, que é o método usado para notificar os usuários. Esta classe também implementa o método Subscribe, que retorna um IDisposable. Esse retorno será capaz de fazer o cancelamento da assinatura do usuário, como visto no método da Listagem 10.

Listagem 10. Método Disposable

public class Disposer : IDisposable
  {
         private List<IObserver<Email>> _usuarios;
         private IObserver<Email> _usuario;
     
  public Disposer(List<IObserver<Email>> usuarios, IObserver<Email> usuario)
  {
      _usuarios = usuarios;
      _usuario = usuario;
  }
   
  public void Dispose()
  {
      if (_usuarios.Contains(_usuario))
          _usuarios.Remove(_usuario);
  }
  }

Note que o método Dispose, usado para remover a assinatura, retira um IObserver da lista.

Por fim, iremos construir as classes que irão receber o e-mail e, para este fim, elas terão que herdar a classe IObserver, que contém os seguintes métodos:

  1. OnCompleted() – Notifica aos observadores que o provider terminou de enviar todas as notificações. Assim, quando implementado, pode chamar, opcionalmente, o método Dispose do objeto IDisposable, que foi devolvido ao observador quando chamouo método IObservable <T> ;
  2. OnError(Exception error) – Método utilizado para informar aos observadores que algum erro ocorreu.
  3. OnNext(T value) – fornece um novo valor aos observadores (usaremos somente este item).

Parece um pouco complicado, mas com o exemplo da Listagem 11 ficará mais claro. Observe as seguintes classes.

Listagem 11. Implementação dos métodos da classe IObserver

public class UsuarioA : IObserver<Email>
         {
          private IDisposable _disposer;
   
          public UsuarioA(IObservable<Email> controladorEmail)
          {
              _disposer = controladorEmail.Subscribe(this);
          }
   
          public void OnCompleted()
          {
              throw new NotImplementedException();
          }
   
          public void OnError(Exception error)
          {
              throw new NotImplementedException();
          }
   
          public void OnNext(Email value)
          {
              Console.WriteLine(value.Descricao + "A");   
          }
   
          public void Dispose()
          {
              _disposer.Dispose();
          }
  }
     
  public class UsuarioB : IObserver<Email>
      {
          private IDisposable _disposer;
          public UsuarioB(IObservable<Email> controladorEmail)
          {
              _disposer = controladorEmail.Subscribe(this);
          }
          public void OnCompleted()
          {
              throw new NotImplementedException();
          }
   
          public void OnError(Exception error)
          {
              throw new NotImplementedException();
          }
   
          public void OnNext(Email value)
          {
              Console.WriteLine(value.Descricao + "B");
          }
               
          public void Dispose()
          {
              _disposer.Dispose();
          }    
      }
    public class UsuarioC : IObserver<Email>
      {
          private IDisposable _disposer;
   
          public UsuarioC(IObservable<Email> controladorEmail)
          {
              _disposer = controladorEmail.Subscribe(this);
          }
          public void OnCompleted()
          {
              throw new NotImplementedException();
          }
   
          public void OnError(Exception error)
          {
              throw new NotImplementedException();
          }
   
          public void OnNext(Email value)
          {
              Console.WriteLine(value.Descricao + "C");
          }
               
          public void Dispose()
          {
              _disposer.Dispose();
          }
}

Note que no construtor de cada classe contém um IObservable responsável por registrar os usuários a serem notificados. Já o método next é a implementação de como o cada usuário será noticiado.

Para o método Main do programa temos o código da Listagem 12.

Listagem 12. Método Main do exemplo 3

  static void Main(string[] args)
          {
             var email = new Email();
   
             var controladorEmail = new ControladorEmail(email);
   
             Console.WriteLine("Os usuarios A, B e C cadastraram-se para receber as promoções. \n");
   
             var usuarioA = new UsuarioA(controladorEmail);
             var usuarioB = new UsuarioB(controladorEmail);
             var usuarioC = new UsuarioC(controladorEmail);
   
             Console.WriteLine("Enviando os emails para os usuários assinados (usuários cadastrados).\n");  
   
             controladorEmail.EnviarEmail();
   
             Console.WriteLine("\nO usuário A resolveu cancelar a assinatura e não irá receber mais emails.\n");
   
             usuarioA.Dispose();
   
             Console.WriteLine("Enviando os emails para os usuários assinados.\n");
             controladorEmail.EnviarEmail();
   
             Console.ReadKey();
              
}

Analisando o código da Listagem 12, note que foi instanciada a classe controladorEmail e as classes de usuário, sendo que, para cada usuário, a classe controladorEmail é passada via construtor. Logo, quando executamos o método EnviarEmail, todos os usuários serão informados e, caso um usuário não queira ser mais notificado, basta executar o método Dispose.

Como resultado, temos a tela de execução da Figura 3.

Execução
do exemplo 3

Figura 3. Execução do exemplo 3

Veja que não é difícil de implementar o padrão de projeto Observer. Como é uma boa prática, o framework .NET deixa o mesmo pronto para usarmos.

Espero que tenham gostado e até a próxima.

Links

Design Pattern
https://msdn.microsoft.com/en-us/library/ee817669.aspx

Padrão de projeto Observer
https://msdn.microsoft.com/pt-br/library/ee850490(v=vs.110).aspx