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.


public interface ISubject   
{
  void Registrar(IObserver observer);
  void Remover(IObserver observer);
  void EnviarEmail();
}
Listagem 1. Interface ISubject

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


public interface IObserver
{
   void ReceberEmail();
}
Listagem 2. Interface IObserver

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


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();
           }
    }
}
Listagem 3. Implementando as interfaces das Listagens 1 e 2

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.


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");
          }
}
Listagem 4. Classe Subject

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.


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();
  }
}
Listagem 5. Método Main

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.


public class ControladorEmail {
      public Action ProcessarEmail;
   
      public void EnviarEmail()
      {
          ProcessarEmail();
      }
}
Listagem 6. Classe ControladorEmail

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


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;
          }
}
Listagem 7. Classes dos tipos de usuários que receberam os e-mails

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


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();
 
 }
Listagem 8. Método Main do exemplo 2

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 que fornecem um mecanismo generalizado para notificação baseada em envio,que são:

  1. IObservable, que é o provedor para notificação de envio;
  2. IObserver, 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, 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.


// 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);
      }
  }
}
Listagem 9. Classe ControladorEmail

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.


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);
}
}
Listagem 10. Método Disposable

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 chamou o método IObservable ;
  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.


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();
      }
}
Listagem 11. Implementação dos métodos da classe IObserver

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.


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();
    
}
Listagem 12. Método Main do exemplo 3

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.