A área de tecnologia se expande cada vez mais, tornando-se um mercado bastante aquecido para…
Inversão de controle Parte 3: Observers
Olá! Tudo bem? Este é o terceiro artigo de uma série sobre inversão de controle. No primeiro artigo, abordamos aspectos conceituais desta técnica. No segundo, discutimos sobre como fábricas podem nos ajudar na implementação deste conceito. Neste artigo, vamos entender o design pattern observer e porque ele faz parte desta lista de artigos.
Agora você pode ouvir esse artigo! Aproveite o player abaixo para conferir a leitura do conteúdo:
Antes que eu me esqueça: este artigo está sendo escrito pensando na leitura narrada (se você preferir, pode ouvir o artigo ao invés de ler). Por isso, qualquer feedback sobre a sua experiência de leitura ou audição serão muito bem-vindos para que possamos melhorar o estilo e a narrativa. Ok?
Disclaimer
No artigo anterior eu tinha dito que apresentaria a vocês o padrão Service Locator. Mas em uma análise melhor, identifiquei que didaticamente este seria o melhor momento para falarmos do padrão observer. Então peço desculpas aos leitores pela troca excepcional.
Por que Observers?
Quando tentamos aplicar todas as boas práticas de codificação, inevitavelmente vamos ter um número grande de classes cooperando entre si. Esta cooperação leva a uma dependência intrínseca entre as classes, levando ao indesejável acoplamento.
Vamos supor, por exemplo, que você construiu um software que está responsável ler o estado de um objeto através de uma API qualquer. Quando alguma mudança de estado acontecer, você precisa atualizar os dados em tela. Do contrário, nada acontece. Para escrever esse código, sem utilizar observers, a classe responsável por verificar a API deveria conhecer a classe responsável por mostrar os dados em tela. Isso, de imediato, geraria uma dependência.
Mas o requisito mudou. E agora, além de mostrar as informações em tela, você terá que enviar uma notificação push para o usuário. E agora? Vamos aumentar mais o acoplamento entre as classes? E como os requisitos não param de mudar, ao invés de notificar uma classe, você precisa notificar outro (ou outros) sistemas que o objeto sofreu modificações. E agora?
O padrão observer vem justamente atender a esses casos. Com ele, a classe principal pode apenas notificar que algo aconteceu e todas as dependentes podem reagir a este acontecimento. Isto sem que elas se conheçam.
Mas como o padrão Observer funciona?
Não é possível entrar em detalhes profundos sobre como implementar este padrão. Do contrário, teríamos um capítulo e não um artigo. Mas vou tentar explicar pra você a forma mais simples de construir um observer.
Passo 1: Crie a interface observer.
Esta interface terá apenas o método Update. Este método será chamado toda vez que a mensagem deve ser transmitida. Continuando com o exemplo que citamos antes, o método Update será chamado todas as vezes que uma alteração de estado acontecer no objeto da API. As classes que cooperam devem, portanto, implementar esta interface.
namespace Observer
{
interface IObserver
{
void Update(ObjetoDTO objeto);
}
}
https://github.com/ftathiago/ArtigoIOC/blob/master/IOCParte3/Observer/IObserver.cs
Talvez você precise de informações para executar as ações dependentes, como por exemplo, para qual estado o objeto se modificou? Assim é ser uma boa ideia que o método update receba informações sobre o objeto observado.
using System;
namespace Observer
{
class NotificadorConsole : IObserver
{
public void Update(ObjetoDTO objeto)
{
Console.WriteLine($"Notificando no console: Nome {objeto.Nome}; Estado: {objeto.Estado}");
}
}
}
https://github.com/ftathiago/ArtigoIOC/blob/master/IOCParte3/Observer/NotificadorConsole.cs
Passo 2: Crie uma interface que atue como subject.
Esta interface deve conter basicamente três métodos: Adicionar, Remover e Notificar. Os métodos Adicionar e Remover recebem, como parâmetro, instâncias da interface observer que criamos no passo 1. O método “Notificar” deve receber, como parâmetro, o mesmo tipo que a interface observer.
namespace Observer
{
interface ISubject
{
void Adicionar(IObserver observer);
void Remover(IObserver observer);
void Notificar(ObjetoDTO objeto);
}
}
https://github.com/ftathiago/ArtigoIOC/blob/master/IOCParte3/Observer/ISubject.cs
Desta forma, a classe que implementar a interface subject terá que se preocupar em armazenar a lista de observers. Quando receber notificações, por meio do método “Notificar”, a classe irá iterar sobre a lista de observers. Para cada item da iteração, será chamado o método Update, passando como parâmetro o mesmo objeto recebido pelo método.
using System;
namespace Observer
{
class Cliente
{
public Cliente(ISubject subject)
{
_subject = subject;
}
public void Executar()
{
Console.WriteLine("Informe o nome:");
var nome = Console.ReadLine();
Console.WriteLine("Informe o número para o estado:");
var stringEstado = Console.ReadLine();
if (!int.TryParse(stringEstado, out var estado))
Console.Write("Você não digitou um número!");
var objeto = new ObjetoDTO
{
Nome = nome,
Estado = estado
};
_subject.Notificar(objeto);
}
private ISubject _subject;
}
}
https://github.com/ftathiago/ArtigoIOC/blob/master/IOCParte3/Observer/Subject.cs
Passo 3: Utilizando o observer
Existem várias formas de utilizar o observer. Através de um Mediator, onde ele manipula as classes e emite a notificação. Também pode injetar o subject na classe cliente como dependência. Você teria, como dependência, apenas uma interface e não uma multidão de classes. E o melhor: nenhuma das pontas precisa conhecer.
No exemplo a seguir, nós criamos duas instâncias de observer (NotificadorConsole e NotificadorBlueTooth) e a adicionamos à instância de subject que também criamos. A classe Cliente recebe uma instância do subject no construtor. Internamente, quando necessário, a classe Cliente dispara a notificação e o subject notifica as instâncias anexadas a ele.
using System;
namespace Observer
{
class Program
{
static void Main(string[] args)
{
var notificadorConsole = new NotificadorConsole();
var notificadorBlueTooth = new NotificadorBlueTooth();
var subject = new Subject();
subject.Adicionar(notificadorConsole);
subject.Adicionar(notificadorBlueTooth);
var cliente = new Cliente(subject);
cliente.Executar();
Console.WriteLine();
Console.WriteLine("Vamos executar sem o bluetooth");
Console.WriteLine();
subject.Remover(notificadorBlueTooth);
cliente.Executar();
Console.ReadLine();
}
}
}
https://github.com/ftathiago/ArtigoIOC/blob/master/IOCParte3/Observer/Program.cs
using System;
namespace Observer
{
class Cliente
{
public Cliente(ISubject subject)
{
_subject = subject;
}
public void Executar()
{
Console.WriteLine("Informe o nome:");
var nome = Console.ReadLine();
Console.WriteLine("Informe o número para o estado:");
var stringEstado = Console.ReadLine();
if (!int.TryParse(stringEstado, out var estado))
Console.Write("Você não digitou um número!");
var objeto = new ObjetoDTO
{
Nome = nome,
Estado = estado
};
_subject.Notificar(objeto);
}
private ISubject _subject;
}
}
https://github.com/ftathiago/ArtigoIOC/blob/master/IOCParte3/Observer/Cliente.cs
Onde está a IoC aqui?
A principal função do padrão observer é distribuir o fluxo do sistema. Como as classes não se conhecem, não há um controle direto do fluxo. A classe que dispara a notificação sequer conhece quantas outras estão observando as suas modificações.
O mesmo acontece com aplicações distribuídas. Os sistemas que implementam o padrão pub-sub permitiram escalabilidade e independência entre as aplicações. DDD possui uma dependência implícita do padrão observer. Sem ele, é quase impossível fazer com que os diversos domínios se comuniquem.
Como já tenho dito ao longo desses artigos, inversão de controle é justamente inverter o controle do fluxo da aplicação. O padrão observer, como vimos, toma para si o controle do fluxo, orquestrando as comunicações entre as classes e os diversos processos. Dependendo da implementação, você ainda pode juntar observers e threads, tornando essa comunicação ainda mais rápida.
Uma última palavra sobre os padrões de projeto
Antes de concluir, preciso trazer uma afirmação que pode acalentar a mente de vocês. Os padrões de projetos não precisam ser implementados ipsis literis iguais ao livro. Adaptações, quando bem feitas, são efetivas e bem vindas. O que você precisa ter realmente em mente é o tipo de problema que o padrão busca resolver e a mecânica que ele implementa. Se você não tiver muita experiência em desenvolvimento com os padrões de projeto, apenas tente não se afastar muito das ideias originais.
Por hoje é só. E no próximo artigo sim, veremos como implementar o padrão Service Locator.
Sobre o autor
Francisco Thiago de Almeida é desenvolvedor há 14 anos, com experiência em vários segmentos públicos e privados. Faz parte do DevParaná, coordenando o meetup DelphiIngá. Hoje está concluindo pós-graduação em arquitetura e desenvolvimento de software na plataforma .Net e integra a unidade de DB1 Global Software.
This Post Has 0 Comments