Como implementar o Design Pattern Observer no .NET

Veja nesse artigo como implementar uma solução de envio de emails usando o padrão de projeto Observer no .NET.

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.

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.

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.

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.

Artigos relacionados