Como aplicar padrões de design Observer em um projeto React Js?

Como aplicar padrões de design Observer em um projeto React Js?

Primeiramente, o que é o Padrão de Projeto Observer?

É um algoritmo feito para um comportamento específico que resolve um dos problemas que geralmente acontecem na programação. Esse padrão sincroniza um assunto principal com outros objetos chamados observadores em outros lugares do aplicativo. Sincronize ambas as partes para que esses outros objetos sejam notificados de qualquer evento que ocorra com o assunto principal.

Portanto, esse padrão permite que você crie um mecanismo de assinatura para notificar os objetos inscritos sobre os eventos que ocorrem com o assunto principal.

No diagrama a seguir, mostrarei seu comportamento.

Na primeira caixa há um sujeito, que se encarregará de salvar todos os objetos que estão inscritos. Ele também se encarregará de cancelar a inscrição e, por último, notificará os observadores quando algo acontecer com o assunto principal.

Qual problema isso resolve?

O padrão de projeto do observador aborda o problema que comumente temos no comportamento assíncrono. Muitas vezes, sobrecarregamos o método "useEffect" quando o levamos para escutar o comportamento de determinados eventos ou dados que sofrem alterações em pouco tempo.

Quando abusamos do método "useEffect", nossa aplicação sofre alterações de desempenho, o que também afeta nosso código, que se torna uma bagunça devido à integração de muitos desses métodos citados.

O padrão observador chega para resolver esse problema implementando o mecanismo de assinatura e notificação, isso torna nosso código mais ordenado, limpo, testável e escalável através de métodos precisos que atuam assinando um objeto principal. Se sofrer uma alteração, notifica todos os assinantes (watchers), que reagem à notificação dada.

Exemplo para aplicar este padrão

Imagine ter um aplicativo de notificação onde mais de 10 chegam a cada segundo. Quando estes chegarem, reagiremos a este evento em todos os componentes que teremos na visão de nossa aplicação. Para este exemplo vamos colocar quatro seções.

Vamos levar em conta também que nossos componentes estarão aguardando quando essas notificações chegarem e cada um realizará uma ação diferente. Nesse caso, cada componente exibirá um número aleatório entre 1 e 1000.

Comumente, resolvemos o acima usando uma loja, seja com redux, context, etc. e um "useEffect" em cada componente, para ouvir quando as notificações são adicionadas à loja.

Vários problemas surgem aqui:

  • Todos os componentes serão acoplados à loja redux.
  • Cada "useEffect" é disparado muitas vezes por segundo. Isso faz com que o componente perca desempenho e ainda mais se tivermos mais de quatro componentes aguardando essas alterações na loja.
  • Não é escalável. Cada função que você deseja integrar no futuro será acoplada ao "useEffect".
  • Será difícil testar funções acopladas ao método "useEffect".

A solução está no padrão Observer!

Agora imagine ter um data center (principal assunto) onde você irá armazenar todas as notificações que chegam de alguma API. Este será o assunto principal.

Cada componente que quiser realizar uma mudança/ação quando uma notificação chegar irá se inscrever no objeto principal, mas agora ele não estará mais escutando, mas será notificado quando uma nova notificação chegar e irá realizar as "n" ações que ele deseja realizar, ao notificá-lo de um novo evento.

Com essa implementação, ele é completamente desacoplado, portanto, seria escalável e testável.

Como implementá-lo?

Veremos agora como implementar este padrão com o mesmo problema. Usaremos o typescript para criar nosso código com o tipo correto. Primeiro, vamos criar nosso projeto em React.

Agora vamos criar dentro do nosso projeto a estrutura de pastas para nossa solução.

A próxima estrutura da nossa solução é a seguinte:

  • Componentes: Nesta pasta estarão localizadas as quatro seções da nossa página principal.
  • Hooks: Aqui estará a lógica independente de cada seção. Nesse caso, há apenas um, que retorna um número aleatório para exibir em cada componente.
  • Páginas: Aqui teremos nossa página principal, que chamará cada seção para exibi-la na tela do navegador.
  • Utils: Nesta pasta vamos colocar nossa lógica para ter nosso assunto, observador e uma classe singleton para instanciar nossa classe assunto apenas uma vez.

Já temos nossa estrutura! Agora veremos nossa implementação.

Agora vamos criar nosso watcher, que irá realizar uma ação independente de quando for notificado.

utils/notification/observer.ts

import { INotificationSubject } from "./notification";
 
/**
* The Observer interface declares the update method, used by subjects.
*/
export interface Observer {
 // Receive update from subject.
 update(subject: INotificationSubject): void;
}
 
/**
* Concrete Observers react to the updates issued by the Subject they had been
* attached to.
*/
export class NotificationObserver implements Observer {
 private action: () => void;
 constructor(action: () => void) {
   this.action = action;
 }
 public update(subject: INotificationSubject): void {
   this.action();
 }
}

Já criamos nossa interface a partir da estrutura de nossa classe. Em seguida, criamos nossa classe Observer, que receberá uma ação em seu construtor e, quando notificada, executará essa ação recebida em sua atualização.

Criaremos nosso assunto principal pelo qual os observadores irão se inscrever e notificá-los.

utils/notification/notification.ts

import { Observer } from "./observer";
 
/**
* The INotificationSubject interface declares a set of methods for managing subscribers.
*/
export interface INotificationSubject {
 // subscribe an observer to the INotificationSubject.
 subscribe(observer: Observer): void;
 
 // Detach an observer from the INotificationSubject.
 unSubcribe(observer: Observer): void;
 
 // Notify all observers about an event.
 notify(): void;
}
 
/**
* The Subject owns some important state and notifies observers when the state
* changes.
*/
export class NotificationSubject implements INotificationSubject {
 /**
  * @type {Observer[]} List of subscribers. In real life, the list of
  * subscribers can be stored more comprehensively (categorized by event
  * type, etc.).
  */
 public observers: Observer[] = [];
 public notificationsNumber: number = 0;
 
 /**
  * The subscription management methods.
  */
 public subscribe(observer: Observer): void {
   const isExist = this.observers.includes(observer);
   if (isExist) {
     return console.log("Subject: Observer has been attached already.");
   }
 
   console.log("Subject: Attached an observer.");
   this.observers.push(observer);
 }
 
 public unSubcribe(observer: Observer): void {
   const observerIndex = this.observers.indexOf(observer);
   if (observerIndex === -1) {
     return console.log("Subject: Nonexistent observer.");
   }
 
   this.observers.splice(observerIndex, 1);
   console.log("Subject: Detached an observer.");
 }
 
 /**
  * Trigger an update in each subscriber.
  */
 public notify(): void {
   this.notificationsNumber += 1;
   this.observers.forEach((observer) => {
     observer.update(this);
   });
 }
}

Nesse arquivo, criamos nossa interface INotificationSubject para declarar nossa estrutura de assunto principal, que terá 3 métodos: subscribe, unsubscribe e notify. Na classe de assunto Notification, adicionaremos duas propriedades Observers e notificationsNumber.

O primeiro salvará todos os observadores que se inscreverem, que farão o papel de assinantes. Um observador é um assinante quando está armazenado neste data center.

Agora o notificationsNumber acompanhará o número total de notificações que chegaram ao sistema.

Os seguintes métodos estão agora definidos:

  • Subscribe: é usado para que se inscrevam no assunto e sejam notificados em algum momento.
  • Unsubscribe: remove o observador dos inscritos.
  • Notificar: É responsável por passar por todos os assinantes para notificá-los, executando o método de atualização de cada observador (assinante).

A seguir, vamos gerar nossa classe singleton para criar, uma vez, a instância do assunto principal.

utils/notification/notificationSingleton.ts

import { NotificationSubject } from "./notification";
 
export class NotificationSingleton extends NotificationSubject {
 private static instance: NotificationSingleton;
 
 /**
  * The NotificationSingleton's constructor should always be private to prevent direct
  * construction calls with the `new` operator.
  */
 private constructor() {
   super();
 }
 
 /**
  * The static method that controls the access to the NotificationSingleton instance.
  *
  * This implementation let you subclass the NotificationSingleton class while keeping
  * just one instance of each subclass around.
  */
 public static getInstance(): NotificationSingleton {
   if (!NotificationSingleton.instance) {
     NotificationSingleton.instance = new NotificationSingleton();
   }
   return NotificationSingleton.instance;
 }
}

Essa classe estende NotificationSubject para conter todos os seus métodos e propriedades (assunto principal). Da mesma forma, com o método getInstance servirá para validar uma única instância, criando-se caso não exista.

Desta forma, teremos um único assunto principal.

Vamos agora criar um arquivo principal para abrigar todas as nossas classes.

utilis/notification/index.ts

export * from "./notification";
export * from "./notificationSingleton";
export * from "./observer";

É hora de fazer o mesmo com nosso hook, usado por todos os componentes.

hooks/useCount.tsx

import { useState } from "react";
 
const randomNumber = (min: number, max: number) => {
 // min and max included
 return Math.floor(Math.random() * (max - min + 1) + min);
};
 
export const useCount = () => {
 const [count, setCount] = useState(0);
 
 const randomCount = () => {
   setCount(randomNumber(0, 1000));
 };
 
 return { count, randomCount };
};

Há dois elementos adicionais envolvidos aqui:

  • RandomNumber: Gera um número aleatório entre dois números.
  • useCount: Este é o gancho que precisamos para armazenar o número gerado em um estado. Que retorna o número do estado e a função que gera o número aleatório.

Agora vamos gerar nosso componente Box, um container com estilos para exibir cada seção na tela.

componentes/Box/index.tsx

import React from "react";
 
interface IBox {
 color: string;
 children?: React.ReactNode;
}
 
export const Box: React.FC<IBox> = ({ color, children }) => {
 return (
   <div style={{ display: "flex", flex: 1, background: color, height: 300 }}>
     {children}
   </div>
 );
};

Continuaremos com nossos componentes.

componentes/SectionOne/index.tsx

import { useEffect } from "react";
import { useCount } from "../../hooks/useCount";
import {
 NotificationObserver,
 NotificationSingleton,
} from "../../utils/notification";
 
export const SectionOne = () => {
 const { randomCount, count } = useCount();
 const notification = NotificationSingleton.getInstance();
 const observer = new NotificationObserver(randomCount);
 useEffect(() => {
   notification.subscribe(observer);
   return () => notification.unSubcribe(observer);
 }, []);
 
 return <div>Componente 1 numero random: {count}</div>;
};

components/SectionTwo/index.tsx

import { useEffect } from "react";
import { useCount } from "../../hooks/useCount";
import {
 NotificationObserver,
 NotificationSingleton,
} from "../../utils/notification";
 
export const SectionTwo = () => {
 const { randomCount, count } = useCount();
 const notification = NotificationSingleton.getInstance();
 const observer = new NotificationObserver(randomCount);
 
 useEffect(() => {
   notification.subscribe(observer);
   return () => notification.unSubcribe(observer);
 }, []);
 
 return <div> Componente 2 numero random: {count}</div>;
};

components/SectionThree/index.tsx

import { useEffect } from "react";
import { useCount } from "../../hooks/useCount";
import {
 NotificationObserver,
 NotificationSingleton,
} from "../../utils/notification";
 
export const SectionThree = () => {
 const { randomCount, count } = useCount();
 const notification = NotificationSingleton.getInstance();
 const observer = new NotificationObserver(randomCount);
 
 useEffect(() => {
   notification.subscribe(observer);
   return () => notification.unSubcribe(observer);
 }, []);
 
 return <div>Componente 3, numero random: {count}</div>;
};

components/SectionFour/index.tsx

import { useEffect, useMemo } from "react";
import { useCount } from "../../hooks/useCount";
import {
 NotificationObserver,
 NotificationSingleton,
} from "../../utils/notification";
 
export const SectionFour = () => {
 const { randomCount, count } = useCount();
 const notification = NotificationSingleton.getInstance();
 const observer = new NotificationObserver(randomCount);
 
 useEffect(() => {
   notification.subscribe(observer);
   return () => notification.unSubcribe(observer);
 }, []);
 
 return <div>Componente 4 numero random: {count}</div>;
};

Indico abaixo as principais funções desses componentes:

  • UseCount: É o gancho criado anteriormente para ser usado em um determinado momento. A partir disso, obtemos o randomCount e a contagem.
  • Notificação: A instância do assunto principal é extraída para assinar e, em algum momento, cancelar a assinatura.
  • Observer: nosso observador que pertence ao componente definido, e passamos a função randomCount como uma ação, que será executada quando este observador for notificado pelo NotificationSubject.
  • UseEffect: é usado para assinar o assunto principal (NotificationSubject) apenas uma vez quando o componente é montado. Quando o componente é desmontado, a assinatura é cancelada.
  • Por fim, a contagem de estado é usada e integrada à visualização do componente.

Estamos quase terminando!

Vamos trabalhar em nossa visão principal.

páginas/notificação.tsx

import { useCallback, useEffect, useState } from "react";
import { Box } from "../components/Box";
import { SectionFour } from "../components/SectionFour";
import { SectionOne } from "../components/SectionOne";
import { SectionThree } from "../components/SectionThree";
import { SectionTwo } from "../components/SectionTwo";
import {
 NotificationObserver,
 NotificationSingleton,
} from "../utils/notification";
 
export const NotificationPage = () => {
 const [notifications, setNotifications] = useState(0);
 const notification = NotificationSingleton.getInstance();
 
 const principalObserver = new NotificationObserver(() =>
   setNotifications(notification.notificationsNumber)
 );
 
 const AsyncInterval = useCallback(() => {
   setInterval(() => {
     notification.notify();
   }, 1000);
 }, []);
 
 useEffect(() => {
   notification.subscribe(principalObserver);
   AsyncInterval();
   return () => notification.unSubcribe(principalObserver);
 }, []);
 
 return (
   <>
     <h1>
       <strong>Numero de notificationes: {notifications}</strong>
     </h1>
     <div style={{ display: "flex", flex: 1, flexDirection: "row" }}>
       <Box color="#F28177">
         <SectionOne />
       </Box>
       <Box color="#F2EA79">
         <SectionTwo />
       </Box>
     </div>
     <div style={{ display: "flex", flex: 1, flexDirection: "row" }}>
       <Box color="#F2EA79">
         <SectionThree />
       </Box>
       <Box color="#F25CD9">
         <SectionFour />
       </Box>
     </div>
   </>
 );
};

UseState: Criamos um estado para salvar as notificações obtidas.

  • Notification: A instância NotificationSubject é obtida para fazer uso de sua propriedade notificationNumber e seus métodos subscribe e unsubscribe.
  • principalObserver: É criado para salvar o total de notificações no estado toda vez que as notificações chegam.
  • AsyncInterval: Ele atuará como uma solicitação de API a cada certo tempo (a cada segundo) e notificará os observadores de sua resposta.
  • UseEffect: É usado para assinar, remover sua assinatura quando o componente for desmontado e, por fim, executar a função AsynInterval. Todo este processo é feito apenas uma vez (quando o componente estiver montado).

Em seguida, ele retorna uma visualização com o número total de notificações e as quatro seções distribuídas na tela.

Aqui acabamos chamando nossa view principal no arquivo raiz do projeto: app.tsx

import "./App.css";
import { NotificationPage } from "./pages/notification";
 
function App() {
 return (
   <div className="App">
     <NotificationPage />
   </div>
 );
}
 
export default App;

Agora temos nosso aplicativo de notificações que contém 4 seções, que realizam uma ação independente quando notificadas de uma nova entrada no assunto principal.

Conclusão

Dessa forma, não sobrecarregamos o método useEffect do React ou fazemos uso indevido dele. Pelo contrário: implementando o padrão Observer resolvemos o problema de sincronizar diferentes partes da aplicação em direção a um objeto definido, o que por sua vez garante que nosso código seja compreensível, escalável e testável.

Exemplo compartilhado no github