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:
- Somente os usuários que se cadastraram no newsletter do site serão avisados das promoções;
- O usuário pode cancelar o envio das promoções quando quiser;
- 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();
}
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();
}
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();
}
}
}
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");
}
}
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();
}
}
Podemos ver a execução do projeto na Figura 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();
}
}
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;
}
}
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();
}
O resultado desse exemplo pode ser visto na Figura 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:
- IObservable, que é o provedor para notificação de envio;
- 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);
}
}
}
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);
}
}
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:
- 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 ;
- OnError(Exception error) – Método utilizado para informar aos observadores que algum erro ocorreu.
- 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();
}
}
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();
}
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.
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
-
Artigo
-
Artigo
-
Artigo
-
Artigo
-
Artigo