Artigo original: Singleton Design Pattern – How it Works in JavaScript with Example Code

Em algum momento, você vai precisar utilizar o estado global nas suas aplicações em React. Ele permite que você tenha todos os dados em um só lugar e faz com que os componentes necessários possam acessá-los facilmente.

Para ajudá-lo a fazer isso, você geralmente usará algum tipo de biblioteca de gerenciamento de estado, como o Redux, React Context ou o Recoil.

Neste artigo, porém, vamos aprender sobre o gerenciamento do estado global com a ajuda de padrões de projeto.

Veremos o que são padrões de projeto e nos concentraremos especificamente no padrão de projeto Singleton. No final, veremos um exemplo prático desse padrão, juntamente com suas vantagens e desvantagens.

Então, sem mais delongas, vamos começar!

Sumário

Pré-requisitos

Antes de passar por este artigo, eu recomendo você estar familiarizado com o conteúdo dos seguintes artigos:

O que é um padrão de projeto?

ezgif.com-gif-maker--9-
Padrões de projeto fornecem soluções conceituais para problemas comuns

Um padrão de projeto é um conjunto generalizado de instruções que solucionam problemas comuns no projeto de um software.

Você pode pensar em padrões de projeto como um site que reúne vários templates de projeto, e que você pode usar cada um deles para ajudar a criar o seu site, baseado nas necessidades específicas dele.

Por isso, a pergunta agora é – por que é importante conhecer os padrões de projeto? Bem, usar os padrões de projeto nos dá vários benefícios, como:

  • Eles são padrões comprovados – ou seja, essas instruções foram testadas muitas e muitas vezes e refletem a experiência e conhecimento de muitos desenvolvedores.
  • São padrões que você pode reutilizar facilmente.
  • Eles são altamente expressivos.

Observe que um padrão de projeto fornece apenas uma solução conceitual para um problema recorrente, de modo otimizado. Ele não é um trecho de código que você pode simplesmente copiar e colar no seu projeto.

Agora que sabemos o que são padrões de projeto, vamos mergulhar de cabeça em nosso primeiro padrão de projeto.

O que é o padrão de projeto Singleton?

singleton-def-gif
O padrão de projeto Singleton expõe uma única instância que pode ser usada por vários componentes

Singleton é um padrão de projeto que nos diz que podemos criar apenas uma instância de uma classe e essa instância pode ser acessada globalmente (e não somente por um componente).

Ele é um dos tipos básicos de padrão de projeto, pois garante que a classe atue como a única fonte de entrada para todos os componentes consumidores que desejam acessar esse estado. Em outras palavras, ele fornece um ponto de entrada comum para usar o estado global.

Portanto, uma classe Singleton deve ser aquela que:

  • Garanta que haja apenas uma instância da classe
  • Forneça um ponto de acesso global ao estado
  • Garanta que a instância seja criada apenas uma única vez

Exemplo do padrão de projeto Singleton

Para entender melhor esse conceito, vejamos um exemplo. Esta será uma aplicação em React simples, que demonstrará como o valor do estado global é usado nos componentes, como fazemos para alterá-lo e como ele é atualizado em todos os componentes. Vamos começar.

Antes de começarmos com a implementação real, vamos dar uma olhadinha na estrutura de pastas e arquivos do nosso projeto:

.
├── index.html
├── package.json
└── src
    ├── componentA.js
    ├── componentB.js
    ├── globalStyles.js
    ├── index.js
    ├── styles.css
    └── utilities.js
Estrutura de pastas do projeto

Estes são os detalhes de cada arquivo:

  • componentA.js é um componente consumidor que usa a classe Singleton para acessar o objeto de estado global e manipulá-lo.
  • componentB.js é semelhante ao componente anterior, pois precisará acessar o objeto de estado global e também pode manipulá-lo.
  • globalStyles.js é um módulo que consiste na classe Singleton e que exporta a instância dessa classe.
  • index.js é responsável por gerenciar as operações globais do JS, ou seja, as alterações do JavaScript necessárias para outros elementos do DOM.
  • styles.css gerencia o estilo da aplicação. É um CSS básico.
  • utilities.js é um módulo que exporta algumas funções utilitárias.
  • index.html consiste no código HTML para os componentes que são necessários no projeto.
  • package.json é uma configuração padrão feita pelo comando npm init.

Agora que sabemos o que cada arquivo faz, podemos começar a implementá-los um a um.

Antes de fazer isso, contudo, precisamos entender o fluxo do código. O objetivo do nosso exemplo é construir uma aplicação em JavaScript que demonstre como o estilo global color é consumido por cada um dos componentes e como cada componente o altera.

Cada componente tem um color-picker(seletor de cor). Quando você altera a propriedade global color do estilo por meio do seletor de cores presente dentro de cada componente, a cor aparece automaticamente em outros componentes e no estado global.

Primeiro, vamos criar o arquivo index.html. Cole o seguinte código no arquivo:

<!DOCTYPE html>
<html>
  <head>
    <title>Parcel Sandbox</title>
    <meta charset="UTF-8" />
    <link rel="stylesheet" href="./src/styles.css" />
  </head>

  <body>
    <div class="global-state">
      <h3>Global State</h3>
      <h4>Color</h4>
      <span id="selected-color"></span>
    </div>
    <div class="contents">
      <div class="component-a">
        <strong>Component A</strong>
        <div>Pick color</div>
        <span id="selected-color">black</span>
        <input type="color" id="color-picker-a" />
      </div>
      <div class="component-b">
        <strong>Component B</strong>
        <div>Pick color</div>
        <span id="selected-color">black</span>
        <input type="color" id="color-picker-b" />
      </div>
    </div>
    <script src="src/index.js"></script>
    <script src="src/componentA.js"></script>
    <script src="src/componentB.js"></script>
  </body>
</html>

Na parte superior, carregamos nosso CSS via <link rel="stylesheet" href="./src/styles.css" />.

Em seguida, dividimos nossa aplicação em duas partes por meio de duas classes:

  • .global-state: que representará o código HTML para mostrar o estado global atual da aplicação.
  • .contents: que representará o código HTML que são os dois componentes.

Cada um dos componentes (component-a e component-b) tem um elemento input (entrada) do tipo seletor de cor.

Ambos os componentes possuem um elemento span com a classe selected-color, que ajudará a exibir o valor atual da variável de estado global color.

Como você pode ver, quando se muda o valor do seletor de cores dentro do componentA, também mudam junto:

  • O texto dentro do elemento span com a classe .selected-color, que fica dentro do componentB e do estado global.
  • O valor dos seletores de cores de componentA e componentB.

Depois, veremos como todos esses valores estão mudando. Por enquanto, é importante entendermos que, se alterarmos o valor do estado global de um componente, as classes Singleton garantirão que o valor da instância seja atualizado e que todos os componentes que a estiverem consumindo obterão o mesmo valor, pois fazem referência à mesma instância.

Em seguida, crie o arquivo globalStyles.js. Copie o código abaixo e cole nele:

let instance;
let globalState = {
  color: ""
};

class StateUtility {
  constructor() {
    if (instance) {
      throw new Error("New instance cannot be created!!");
    }

    instance = this;
  }

  getPropertyByName(propertyName) {
    return globalState[propertyName];
  }

  setPropertyValue(propertyName, propertyValue) {
    globalState[propertyName] = propertyValue;
  }
}

let stateUtilityInstance = Object.freeze(new StateUtility());

export default stateUtilityInstance;

O trecho de código acima é um módulo que possui uma classe Singleton StateUtility e exporta por padrão a instância da mesma classe.

Vamos olhar com mais detalhes essa classe StateUtility para entender como ela se torna uma classe Singleton:

  • Ela tem um constructor e dois métodos de classe, chamados getPropertyByName e setPropertyValue. Ambos os métodos de classe são autoexplicativos: um obtém o valor da propriedade e o outro define seu valor.
  • Em seguida, temos a função constructor. Ela é uma função que é invocada sempre que criamos um novo objeto dessa classe.
  • Aqui temos um problema: para uma classe ser uma classe Singleton, precisamos nos certificar de que ela crie apenas uma instância de si, e nada mais.
  • Para garantir que isso aconteça, simplesmente criamos uma variável global chamada instance no topo do módulo. Essa variável atuará como um verificador. Adicionamos também uma condição na função constructor de modo que, se a variável instance tiver qualquer valor (ou seja, um objeto da classe StateUtility), ela enviará um erro. Caso não tenha um valor, então atribuirá à variável instance a instância da classe atual (o objeto this).
  • Aqui, implementamos a classe StateUtility para que ela possa expor e alterar a variável globalState.
  • Garantimos, também, que não estamos expondo globalState diretamente, mas usando os métodos de classe StateUtility. Deste modo, protegemos o estado global de ser alterado diretamente.
  • Por fim, criamos a instância da classe assim: let stateUtilityInstance = Object.freeze(new StateUtility());.
  • Usamos também o método Object.freeze para que nenhuma outra classe/componente/módulo seja capaz de modificar nossa instância stateUtilityInstance.

Agora, crie um arquivo chamado componentA.js dentro da pasta src. Copie e cole o código abaixo no arquivo:

import {
    setAllSelectedColor
} from "./utilities";
import globalStyle from "./globalStyles";

// Obter os elementos do DOM respectivos
const selectedColor = document.querySelectorAll("#selected-color");
const colorPickerA = document.getElementById("color-picker-a");
const colorPickerB = document.getElementById("color-picker-b");

// Manipulador de evento para quando um evento de mudança ocorrer
colorPickerA.onchange = (event) => {
	// Define a propriedade color do state global com a cor equivalente ao valor atual do seletor de cor;
    globalStyle.setPropertyValue("color", event.target.value);
    const color = globalStyle.getPropertyByName("color");

    // Uma função que define o valor de todos os elementos do DOM #selection-color;
    setValueOfSimilarElements(selectedColor, color);

    // Certificar-se de definir que o valor do seletor de cor do componente B esteja definido como sendo igual ao valor do seletor de cor de A;
    // Isso é feito para garantir que os dois seletores de cor tenham o mesmo valor no momento da mudança;
    colorPickerB.value = color;
};

Aqui estão os detalhes do código acima:

  • O objetivo desse código é garantir que anexamos o manipulador onChange no seletor de cores, que está presente dentro do arquivo component-a. Nesse caso, o seletor de cores do componentA é identificado pela id: #color-picker-a.
  • Precisamos ter certeza de que esse manipulador:
    1. Defina o valor da propriedade color do globalState.
    2. Busque a mesma propriedade novamente.
    3. Aplique o mesmo valor a diferentes áreas do DOM.
    4. Também garanta que definimos o valor do outro seletor de cores com o estado global.

Agora, vamos dar uma olhada em todos esses passos um a um:

  • Primeiro, vamos buscar todos os elementos DOM necessários.
  • O que estamos planejando aqui é atualizar todos os seletores de cores e elementos span com o id #selected-color com o valor atual da propriedade color do globalState sempre que o evento onchange ocorrer.
  • No caso do componentA, uma vez que alteramos a cor pelo seletor de cores, precisamos atualizar o mesmo valor em 2 elementos span (#selected-color) – ou seja, um elemento span de componentB e um elemento span na div do container da classe .global-state.
  • Fazemos isso porque queremos manter todos os componentes sincronizados e demonstrar que o valor do estado global permanece o mesmo em todos os componentes.
  • Então, atualizamos a propriedade color do estado global usando o método setPropertyValue da classe StateUtility. Passamos para ele o event.target.value que contém o valor atual do seletor de cores que está dentro do input #color-picker-a.
  • Uma vez que o valor está definido, buscamos a mesma propriedade novamente usando getPropertyByName. Fazemos isso para demonstrar que a propriedade color do estado global foi atualizada e está pronta para ser usada.
  • Em seguida, usamos a função utilitária setValueOfSimilarElements para atualizar todos os elementos que possuem o mesmo nome de classe/id com algum valor. Nesse caso, atualizamos todos os elementos #selected-color com o valor color.
  • Por fim, atualizamos o valor do outro seletor de cores, que é o seletor de cores #color-picker-b do componentB,

Fazemos a mesma coisa para o componentB. Criamos o arquivo componentB.js e o atualizamos com este código:

import {
    setValueOfSimilarElements
} from "./utilities";
import globalStyle from "./globalStyles";

// Obter os elementos do DOM respectivos
const selectedColor = document.querySelectorAll("#selected-color");
const colorPickerA = document.getElementById("color-picker-a");
const colorPickerB = document.getElementById("color-picker-b");

/**
 * Manipulador de evento para quando um evento de mudança ocorrer
 */
colorPickerB.onchange = (event) => {
    // Define a propriedade color do state global com a cor equivalente ao valor atual do seletor de cor
    globalStyle.setPropertyValue("color", event.target.value);

    const color = globalStyle.getPropertyByName("color");

    // Uma função que define o valor de todos os elementos do DOM #selection-color;
    setValueOfSimilarElements(selectedColor, color);

    // Certificar-se de definir que o valor do seletor de cor do componente A esteja definido como sendo igual ao valor do seletor de cor de B;
    // Isso é feito para garantir que os dois seletores de cor tenham o mesmo valor no momento da mudança;
    colorPickerA.value = color;
};

Fazemos a mesma coisa que fizemos dentro do arquivo do componentA. Nesse caso , porém, atualizamos o valor do seletor de cores presente dentro do componentA (ou seja, atualizamos o valor do elemento #color-picker-a).

Veja como ficará nossa aplicação:

https://www.canva.com/design/DAFGQsDz_cU/

Segue o link do código completo:

Prós e contras do padrão de projeto Singleton

Aqui estão algumas das vantagens de se usar o padrão de projeto Singleton:

  • Ele garante que apenas uma única instância da classe seja criada.
  • Obtemos um único ponto de acesso para a instância que pode ser acessada globalmente.

Aqui estão algumas desvantagens do padrão de projeto Singleton:

  • Ele viola o princípio de responsabilidade única, pois está tentando resolver dois problemas ao mesmo tempo. Ele tenta resolver os seguintes problemas: Garantir que uma classe tenha apenas uma instância e atribuir um ponto único de acesso global à instância da classe Singleton.
  • É difícil escrever casos de testes unitários para classes Singleton. Isso ocorre porque a ordem de execução pode alterar o valor presente no estado global. Portanto, a ordem de execução é importante.
  • Ao escrever testes unitários, existe o risco de outro componente ou módulo estar alterando o valor/instância do estado global. Em tais cenários, torna-se difícil depurar o erro.

Resumo

O padrão de projeto Singleton é útil na criação de um estado global que possa ser acessado por qualquer componente.

Então, falando brevemente sobre o padrão Singleton:

  • É um padrão que restringe a classe a criar apenas uma instância dela mesma.
  • O padrão Singleton pode ser considerado a base das bibliotecas de gerenciamento de estado global, como o Redux ou o React Context.
  • Ele pode ser acessado ​​globalmente e atua como um único ponto de acesso para acessar o estado global.

Isso é tudo, pessoal. Obrigado pela leitura!

Siga o autor no Twitter , no GitHub e no LinkedIn.