Singleton e observer: padrões de projeto em JavaScript

Singleton e observer: padrões de projeto em JavaScript

Os padrões de projeto surgiram na década de 1990 como uma resposta à necessidade de soluções padronizadas para problemas comuns de design de software. O livro "Design Patterns: Elements of Reusable Object-Oriented Software", publicado em 1994 pelo grupo conhecido como "Gang of Four", formalizou e popularizou os padrões de projeto.

Esses padrões oferecem diretrizes comprovadas para o design de sistemas orientados a objetos, promovendo a reutilização, a flexibilidade e a manutenibilidade do código. Desde então, os padrões de projeto se tornaram uma parte fundamental da Engenharia de Software e ajudam os desenvolvedores a criar sistemas de software mais flexíveis, extensíveis e fáceis de manter, seguindo práticas recomendadas e estabelecendo uma linguagem comum de design.

Os padrões de projeto promovem a modularidade, a separação de responsabilidades e a reutilização de código, contribuindo para a criação de software de alta qualidade.


Nesse artigo vamos introduzir ao leitor alguns padrões de projeto para que entendam de forma ampla como eles podem ser úteis, independente de implementação ou característica particular de cada padrão. Com isso, espera-se que o leitor busque por conta própria conhecer os outros padrões de projeto existentes. Neste artigo falaremos de apenas 2 deles: Singleton e Observer.

Existem algumas discussões na comunidade sobre o uso ou não de alguns padrões. Minha recomendação é que você não perca tempo com isso, principalmente se for iniciante na área, afinal, ponto de vista todos nós temos e mesmo que você ainda não entenda bem sobre padrões de projeto, sugiro que conheça-os e forme sua própria opinião. A ideia é que o leitor não se apegue ao código, mas à finalidade de cada um desses padrões. Isso deve ser dito porque embora o autor deste artigo tenha usado o paradigma orientado a objetos, eles podem ser implementados com o paradigma funcional.

Os padrões de projeto são divididos da seguinte forma:


Singleton

O primeiro padrão de projeto do qual vamos falar é bem simples de ser entendido e implementado, o Singleton. Esse designer pattern busca garantir que exista apenas uma instância de uma classe e dessa forma fornecer apenas um ponto de acesso global para essa instância. Isso é útil em situações em que ter várias instâncias da mesma classe pode causar problemas ou desperdício de recursos, por exemplo, uma conexão com banco de dados, talvez esse seja o exemplo de caso de uso mais clássico para esse padrão de projeto.

Imagine que cada vez que uma requisição HTTP fosse feita ao servidor, uma nova instância de conexão com o banco de dados fosse criada. Isso poderia levar a uma sobrecarga significativa no banco de dados, pois cada instância de conexão exigiria recursos de rede e processamento. Além disso, a criação e destruição contínuas de instâncias de conexão poderiam causar atrasos e impactar negativamente o desempenho do aplicativo.

1) Crie um arquivo singleton.js e insira o código abaixo:

class DatabaseConnection {

constructor() {

// Simulando o banco de dados como um array de usuários

this.users = [];

}

// Método para adicionar um usuário ao banco de dados

addUser(user) {

this.users.push(user);

}

// Método para remover um usuário do banco de dados

removeUser(user) {

const index = this.users.findIndex(u => u.id === user.id);

if (index !== -1) {

this.users.splice(index, 1);

}

}

// Método para obter todos os usuários do banco de dados

getUsers() {

return this.users;

}

}

class SingletonDatabaseConnection {

constructor() {

if (!SingletonDatabaseConnection.instance) {

SingletonDatabaseConnection.instance = new DatabaseConnection();

}

}

getInstance() {

return SingletonDatabaseConnection.instance;

}

}

// Classe que representa um usuário

class User {

constructor(id, name) {

this.id = id;

this.name = name;

}

}

// Uso do Singleton para obter a instância da conexão com o banco de dados

// Instanciando a SingletonDatabaseConnection() duas vezes, podemos provar que apenas uma instância foi criada

const connection1 = new SingletonDatabaseConnection().getInstance();

const connection2 = new SingletonDatabaseConnection().getInstance();

// Ambas as conexões são a mesma instância, ou seja, não foi criada outra instância.

console.log(connection1 === connection2); // true

// Criação de usuários

const user1 = new User(1, 'John');

const user2 = new User(2, 'Alice');

// Vamos manipular as conexões, inserimos o user1 com a connection1 e o user2 com a connection2

connection1.addUser(user1);

connection2.addUser(user2);

// Ao chamar o metodo getUsers da connection1, vamos receber também o user2 que foi inserido no connection2.

// provando assim, que a connection1 e connection2 são a mesma instância e manipulam os mesmos dados.

console.log(connection1.getUsers()); // [User { id: 1, name: 'John' }, User { id: 2, name: 'Alice' }]

// Para finalizar, removemos o user1 pela instância connection2

connection2.removeUser(user1);

// Ao chamar o getUser pela connection1, podemos ver que o método removeUser da connection2 removeu o user1

console.log(connection1.getUsers()); // [User { id: 2, name: 'Alice' }]


2) No seu terminal execute o comando:

node singleton.js

3) A saída deve exibir:

true

[ User { id: 1, name: 'John' }, User { id: 2, name: 'Alice' } ]

[ User { id: 2, name: 'Alice' } ]

Casos de uso para o Singleton

- Conexões com banco de dados: o Singleton pode ser usado para garantir que apenas uma única conexão com o banco de dados seja estabelecida e reutilizada por toda a aplicação, evitando a sobrecarga de abrir e fechar conexões repetidamente.

- Gerenciamento de configurações globais do sistema: O Singleton pode ser utilizado para armazenar e fornecer acesso às configurações globais do sistema, como valores de propriedades ou preferências, que precisam ser acessíveis de forma consistente em vários componentes da aplicação.

- Implementação de gerenciadores de cache centralizados: Com o Singleton, é possível criar um gerenciador de cache centralizado que controla o acesso e a atualização dos dados em cache, garantindo que apenas uma única instância do cache seja mantida em todo o sistema.

- Registro de log centralizado: Um Singleton pode ser usado para criar um registro de log centralizado, onde todas as mensagens de log do sistema são registradas de forma consistente e podem ser acessadas de maneira global para fins de depuração, monitoramento ou auditoria.

- Gerenciadores de recursos compartilhados, como objetos de cache: O Singleton pode ser aplicado para criar e gerenciar recursos compartilhados, como objetos de cache, garantindo que apenas uma única instância desses recursos seja mantida e acessível em toda a aplicação.

Observações do Singleton

O uso do padrão Singleton é discutido na comunidade de desenvolvimento de software devido a algumas considerações:

  • Acoplamento e dependências ocultas: O Singleton cria acoplamento forte entre os componentes, dificultando a substituição e os testes unitários.
  • Testabilidade: O estado global compartilhado do Singleton torna difícil testar componentes dependentes dele. A injeção de dependência pode ser uma abordagem mais testável.
  • Multithreading e concorrência: É necessário garantir que a implementação do Singleton seja thread-safe para evitar problemas de concorrência.
  • Escalabilidade: O Singleton pode limitar a escalabilidade em sistemas distribuídos ou que precisam de múltiplas instâncias.
  • Responsabilidade única: O Singleton deve ter uma responsabilidade clara para evitar a violação do princípio de responsabilidade única (SOLID).
  • Em resumo, o Singleton tem suas vantagens, mas é importante ponderar os impactos e considerar alternativas antes de usá-lo. Avalie as necessidades do sistema e os princípios de design para tomar a melhor decisão.

Observer

O padrão Observer permite que um objeto informe automaticamente outros objetos sobre mudanças relevantes, sem que estes precisem verificar constantemente o objeto observado. Imagine um site de notícias do seu interesse que você se inscreve para receber novas notícias no email.

Os observadores se inscrevem no objeto observável e são notificados quando ocorrem mudanças importantes. Assim, o padrão Observer facilita a comunicação eficiente e desacoplada entre objetos, garantindo que reajam de acordo com suas necessidades específicas.

1)  Crie um arquivo observer.js e insira o código abaixo:

// Classe Observer (Subscriber/Usuário)

class SubscriberUser {

constructor(email) {

this.email = email;

}

// Método chamado quando uma nova newsletter é enviada

update(newsletter) {

console.log(`Enviando e-mail para ${this.email}: Nova newsletter "${newsletter}" disponível.`);

// Lógica real de envio de e-mail aqui

}

}

// Classe Observable (NewsletterSystem)

class NewsletterSystem {

constructor() {

this.subscribers = [];

}

// Método para inscrever um assinante

subscribe(subscriber) {

this.subscribers.push(subscriber);

}

// Método para cancelar a inscrição de um assinante

unsubscribe(subscriber) {

this.subscribers = this.subscribers.filter((sub) => sub !== subscriber);

}

// Método para enviar uma nova newsletter a todos os assinantes

sendNewsletter(newsletter) {

this.subscribers.forEach((subscriber) => {

subscriber.update(newsletter);

});

}

}

// Exemplo de uso

const newsletterSystem = new NewsletterSystem();

// Criando assinantes

const subscriber1 = new SubscriberUser('email1@example.com');

const subscriber2 = new SubscriberUser('email2@example.com');

const subscriber3 = new SubscriberUser('email3@example.com');

// Inscrevendo assinantes no sistema de newsletters

newsletterSystem.subscribe(subscriber1);

newsletterSystem.subscribe(subscriber2);

newsletterSystem.subscribe(subscriber3);

// Enviando uma nova newsletter

newsletterSystem.sendNewsletter('Novidades da Semana');

2) No terminal execute o comando:

node observer.js

3) A saída deve exibir:

Enviando e-mail para email1@example.com: Nova newsletter "Novidades da Semana" disponível.

Enviando e-mail para email2@example.com: Nova newsletter "Novidades da Semana" disponível.

Enviando e-mail para email3@example.com: Nova newsletter "Novidades da Semana" disponível.

Casos de uso para o Observer

  • Botões de inscrição em um site: Vários objetos podem se inscrever para serem notificados quando um botão é clicado.
  • Monitoramento de temperatura: Sensores observáveis podem notificar automaticamente um objeto principal sobre mudanças de temperatura, permitindo que ações apropriadas sejam tomadas.
  • Atualizações de feeds de notícias: Usuários podem se inscrever para receber notificações de novas notícias de diferentes fontes. Cada fonte notifica automaticamente os usuários.


Observações do Observer

  • Overhead de desempenho: O padrão Observer pode ter um certo overhead de desempenho, especialmente em casos em que há um grande número de observadores e/ou notificações frequentes. A notificação de cada observador pode exigir recursos adicionais e processamento, o que pode impactar a eficiência geral do sistema.
  • Risco de vazamentos de memória: Se os objetos observadores não forem devidamente desinscritos ou removidos quando não forem mais necessários, pode ocorrer um vazamento de memória. Isso pode acontecer quando objetos observadores ainda estão registrados no objeto observável, mesmo que não sejam mais relevantes.
  • Complexidade adicional: A implementação do padrão Observer pode adicionar uma complexidade adicional ao código, especialmente se houver múltiplos eventos ou estados que precisam ser observados. Isso pode tornar o código mais difícil de entender e manter, especialmente para projetos de grande porte.
  • É importante avaliar essas desvantagens em relação aos benefícios que o padrão Observer traz para o sistema. Em muitos casos, os benefícios superam as desvantagens, mas é importante considerar cuidadosamente a aplicação do padrão e o impacto que ele pode ter no desempenho e na complexidade do sistema.


Considerações finais

Mais importante do que entender como implementar os padrões de projeto é entender qual a finalidade deles de forma mais abstrata, pois os padrões de projeto podem ser aplicados independentemente de paradigma ou linguagem de programação utilizada, trata-se de adotar uma mentalidade de design orientada a problemas e soluções.

💡
As opiniões e comentários expressos neste artigo são de propriedade exclusiva de seu autor e não representam necessariamente o ponto de vista da Revelo.

A Revelo Content Network acolhe todas as raças, etnias, nacionalidades, credos, gêneros, orientações, pontos de vista e ideologias, desde que promovam diversidade, equidade, inclusão e crescimento na carreira dos profissionais de tecnologia.