Javascript Patterns - Singleton
Singletons são classes que podem ser instanciadas uma vez e podem ser acessadas globalmente. Essa única instância pode ser compartilhada em todo o nosso aplicativo, o que torna os Singletons ótimos para gerenciar o estado global em um aplicativo.
Primeiro, vamos dar uma olhada em como um singleton pode parecer usando uma classe ES2015. Para este exemplo, vamos construir uma classe Counter
que tenha:
- um método
getInstance
que retorna o valor da instância - um método
getCount
que retorna o valor atual da variávelcounter
- um método de
increment
que incrementa o valor docounter
em um - um método de
decrement
que decrementa o valor docounter
em um
let counter = 0;
class Counter {
getInstance() {
return this;
}
getCount() {
return counter;
}
increment() {
return ++counter;
}
decrement() {
return --counter;
}
}
No entanto, esta classe não atende aos critérios para um Singleton! Um Singleton só pode ser instanciado uma vez . Atualmente, podemos criar várias instâncias da classe Counter
.
let counter = 0;
class Counter {
getInstance() {
return this;
}
getCount() {
return counter;
}
increment() {
return ++counter;
}
decrement() {
return --counter;
}
}
const counter1 = new Counter();
const counter2 = new Counter();
console.log(counter1.getInstance() === counter2.getInstance()); // false
Ao chamar o método new
duas vezes, apenas definimos counter1
e counter2
iguais para instâncias diferentes. Os valores retornados pelo método getInstance
em counter1
e counter2
efetivamente retornaram referências a diferentes instâncias: eles não são estritamente iguais!
Vamos garantir que apenas uma instância da classe Counter
possa ser criada.
Uma maneira de garantir que apenas uma instância possa ser criada é criar uma variável chamada instance
. No construtor de Counter
, podemos definir instance
igual a uma referência à instância quando uma nova instância é criada. Podemos evitar novas instanciações verificando se a variável de instância já possui um valor. Se for esse o caso, já existe uma instância. Isso não deveria acontecer: um erro deveria ser gerado para que o usuário soubesse
let instance;
let counter = 0;
class Counter {
constructor() {
if (instance) {
throw new Error("Você só pode criar uma instância!");
}
instance = this;
}
getInstance() {
return this;
}
getCount() {
return counter;
}
increment() {
return ++counter;
}
decrement() {
return --counter;
}
}
const counter1 = new Counter();
const counter2 = new Counter();
// Error: Você só pode criar uma instância!
Perfeito! Não podemos mais criar várias instâncias.
Vamos exportar a instância Counter
do arquivo counter.js
. Mas antes de fazer isso, devemos congelar a instância também. O método Object.freeze
garante que o código de consumo não possa modificar o Singleton. As propriedades na instância congelada não podem ser adicionadas ou modificadas, o que reduz o risco de sobrescrever acidentalmente os valores no Singleton.
let instance;
let counter = 0;
class Counter {
constructor() {
if (instance) {
throw new Error("Você só pode criar uma instância!");
}
instance = this;
}
getInstance() {
return this;
}
getCount() {
return counter;
}
increment() {
return ++counter;
}
decrement() {
return --counter;
}
}
const singletonCounter = Object.freeze(new Counter());
export default singletonCounter;
Vamos dar uma olhada em um aplicativo que implementa o exemplo Counter
. Temos os seguintes arquivos:
counter.js
: contém a classeCounter
e exporta uma instânciaCounter
como sua exportação padrãoindex.js
: carrega os módulosredButton.js
eblueButton.js
redButton.js
: importaCounter
e adiciona o método de incremento de Counter como um ouvinte de evento ao botão vermelho e registra o valor atual decounter
invocando o métodogetCount
blueButton.js
: importaCounter
e adiciona o método de incremento de Counter como um ouvinte de evento ao botão azul e registra o valor atual decounter
invocando o métodogetCount
Ambos blueButton.js
e redButton.js
importam a mesma instância de counter.js
. Esta instância é importada como Counter
em ambos os arquivos.
Quando invocamos o método de increment
em redButton.js
ou blueButton.js
, o valor da propriedade counter
na instância Counter
é atualizado em ambos os arquivos. Não importa se clicamos no botão vermelho ou azul: o mesmo valor é compartilhado entre todas as instâncias. É por isso que o contador continua incrementando em 1, mesmo que estejamos invocando o método em arquivos diferentes.
(Des)vantagens
Restringir a instanciação a apenas uma instância pode economizar muito espaço de memória. Em vez de configurar a memória para uma nova instância a cada vez, só precisamos configurar a memória para aquela instância, que é referenciada em todo o aplicativo. No entanto, Singletons são realmente considerados um antipadrão e podem (ou devem) ser evitados em JavaScript.
Em muitas linguagens de programação, como Java ou C++, não é possível criar objetos diretamente como fazemos em JavaScript. Nessas linguagens de programação orientadas a objetos, precisamos criar uma classe, que cria um objeto. Esse objeto criado tem o valor da instância da classe, assim como o valor da instância no exemplo do JavaScript.
No entanto, a implementação de classe mostrada nos exemplos acima é, na verdade, um exagero. Como podemos criar objetos diretamente em JavaScript, podemos simplesmente usar um objeto regular para obter exatamente o mesmo resultado. Vamos cobrir algumas das desvantagens de usar Singletons!
Gerenciamento de estado em React
No React, geralmente contamos com um estado global por meio de ferramentas de gerenciamento de estado, como Redux ou React Context, em vez de usar Singletons. Embora seu comportamento de estado global possa parecer semelhante ao de um Singleton, essas ferramentas fornecem um estado somente leitura em vez do estado mutável do Singleton. Ao usar o Redux, apenas os reducers
podem atualizar o estado, depois que um componente enviou uma ação por meio de um dispatch
.
Embora as desvantagens de ter um estado global não desapareçam magicamente com o uso dessas ferramentas, podemos pelo menos garantir que o estado global seja alterado da maneira pretendida, pois os componentes não podem atualizar o estado diretamente.