Los singleton son clases de las que se puede crear una instancia una vez y a las que se puede acceder globalmente. Esta instancia única se puede compartir en toda nuestra aplicación, lo que hace que Singletons sea excelente para administrar el estado global en una aplicación.
Primero, echemos un vistazo a cómo puede verse un singleton usando una clase ES2015. Para este ejemplo, vamos a construir una clase
Counter
que tenga:
- un método
getInstance
que devuelve el valor de la instancia - un método
getCount
que devuelve el valor actual de lacounter
variable - un método
increment
que incrementa el valor decounter
en uno - un método
decrement
que disminuye el valor decounter
en uno
let counter = 0;
class Counter {
getInstance() {
return this;
}
getCount() {
return counter;
}
increment() {
return ++counter;
}
decrement() {
return --counter;
}
}
Sin embargo, ¡esta clase no cumple con los criterios para un Singleton! Un Singleton solo debería poder crear una instancia una vez. Actualmente, podemos crear múltiples instancias de la clase
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
Al llamar al método
new
dos veces, simplemente configuramos
counter1
e
counter2
e igualamos a diferentes instancias. Los valores devueltos por el
getInstance
método
counter1
y
counter2
efectivamente devolvieron referencias a diferentes instancias: ¡no son estrictamente iguales!
Asegurémonos de que solo se pueda crear una instancia de la clase.
Counter
Una forma de asegurarse de que solo se pueda crear una instancia es creando una variable llamada
instance
. En el constructor de
Counter
, podemos establecer
instance
igual a una referencia a la instancia cuando se crea una nueva instancia. Podemos evitar nuevas instancias comprobando si la variable
instance
ya tenía un valor. Si ese es el caso, ya existe una instancia. Esto no debería suceder: debería aparecer un error para informar al usuario
let instance;
let counter = 0;
class Counter {
constructor() {
if (instance) {
throw new Error("You can only create one instance!");
}
instance = this;
}
getInstance() {
return this;
}
getCount() {
return counter;
}
increment() {
return ++counter;
}
decrement() {
return --counter;
}
}
const counter1 = new Counter();
const counter2 = new Counter();
// Error: You can only create one instance!
¡Perfecto! Ya no podemos crear varias instancias.
Exportemos la instancia
Counter
desde el archivo
counter.js
. Pero antes de hacerlo, también deberíamos congelar la instancia. El método
Object.freeze
garantiza que el código consumidor no pueda modificar el Singleton.
Las propiedades de la instancia congelada no se pueden agregar ni modificar, lo que reduce el riesgo de sobrescribir accidentalmente los valores en Singleton.
let instance;
let counter = 0;
class Counter {
constructor() {
if (instance) {
throw new Error("You can only create one instance!");
}
instance = this;
}
getInstance() {
return this;
}
getCount() {
return counter;
}
increment() {
return ++counter;
}
decrement() {
return --counter;
}
}
const singletonCounter = Object.freeze(new Counter());
export default singletonCounter;
Echemos un vistazo a una aplicación que implementa el
Counter
ejemplo. Disponemos de los siguientes archivos:
-
counter.js
: contiene la claseCounter
y exporta una instancia **Counter
como su exportación predeterminada -
index.js
: carga los módulosredButton.js
yblueButton.js
-
redButton.js
: importaCounter
y agregaCounter
el métodoincrement
como detector de eventos al botón rojo y registra el valor actual decounter
invocando el métodogetCount
-
blueButton.js
: importaCounter
y agregaCounter
elincrement
método como detector de eventos al botón azul y registra el valor actual decounter
invocando el métodogetCount
Ambos
blueButton.js
e
redButton.js
importar la misma instancia desde
counter.js
. Esta instancia se importa como
Counter
en ambos archivos.
Cuando invocamos el método
increment
en
redButton.js
o
blueButton.js
, el valor de la propiedad
counter
en la instancia
Counter
se actualiza en ambos archivos. No importa si pulsamos en el botón rojo o en el azul: el mismo valor se comparte entre todas las instancias. Es por eso que el contador sigue incrementándose en uno, aunque estemos invocando el método en archivos diferentes.
Compensaciones
Restringir la creación de instancias a una sola instancia podría ahorrar mucho espacio en la memoria. En lugar de tener que configurar la memoria para una nueva instancia cada vez, solo tenemos que configurar la memoria para esa instancia, a la que se hace referencia en toda la aplicación. Sin embargo, los Singleton en realidad se consideran un antipatrón y pueden (o… deberían ) evitarse en JavaScript.
En muchos lenguajes de programación, como Java o C++, no es posible crear objetos directamente como lo hacemos en JavaScript. En esos lenguajes de programación orientados a objetos, necesitamos crear una clase, que crea un objeto. Ese objeto creado tiene el valor de la instancia de la clase, tal como el valor de
instance
en el ejemplo de JavaScript.
Sin embargo, la implementación de la clase que se muestra en los ejemplos anteriores es en realidad excesiva. Dado que podemos crear objetos directamente en JavaScript, simplemente podemos usar un objeto normal para lograr exactamente el mismo resultado. ¡Veamos algunas de las desventajas de usar Singletons!
Usando un objeto normal
Usemos el mismo ejemplo que vimos anteriormente. Sin embargo, esta vez,
counter
es simplemente un objeto que contiene:
- una
count
propiedad - un
increment
método que incrementa el valor decount
en uno - un
decrement
método que disminuye el valor decount
en uno
Dado que los objetos se pasan por referencia, ambos
redButton.js
y
blueButton.js
importan una referencia al mismo
counter
objeto. Modificar el valor de
count
en cualquiera de estos archivos modificará el valor en
counter
, que es visible en ambos archivos.
Pruebas
Probar código que se basa en un Singleton puede resultar complicado. Como no podemos crear nuevas instancias cada vez, todas las pruebas se basan en la modificación de la instancia global de la prueba anterior. El orden de las pruebas es importante en este caso, y una pequeña modificación puede provocar que todo un conjunto de pruebas falle. Después de la prueba, debemos restablecer toda la instancia para restablecer las modificaciones realizadas por las pruebas.
Ocultación de dependencia
Al importar otro módulo,
superCounter.js
en este caso, puede que no sea obvio que el módulo está importando un Singleton. En otros archivos, como
index.js
en este caso, es posible que estemos importando ese módulo e invocando sus métodos. De esta manera, modificamos accidentalmente los valores en el Singleton. Esto puede provocar un comportamiento inesperado, ya que se pueden compartir varias instancias de Singleton en toda la aplicación, todas las cuales también se modificarían.
Comportamiento global
Se debería poder hacer referencia a una instancia Singleton en toda la aplicación. Las variables globales esencialmente muestran el mismo comportamiento: dado que las variables globales están disponibles en el ámbito global, podemos acceder a esas variables en toda la aplicación.
Tener variables globales generalmente se considera una mala decisión de diseño. La contaminación del alcance global puede terminar sobrescribiendo accidentalmente el valor de una variable global, lo que puede generar muchos comportamientos inesperados.
En ES2015, la creación de variables globales es bastante poco común. La palabra clave new
let
y
const
evita que los desarrolladores contaminen accidentalmente el alcance global, al mantener las variables declaradas con estas dos palabras clave en el ámbito de bloque. El nuevo
module
sistema en JavaScript facilita la creación de valores accesibles globalmente sin contaminar el alcance global, al poder almacenar
export
valores de un módulo y
import
esos valores en otros archivos.
Sin embargo, el caso de uso común de un Singleton es tener algún tipo de estado global en toda su aplicación. Hacer que varias partes de su código base dependan del mismo objeto mutable puede provocar un comportamiento inesperado.
Por lo general, ciertas partes del código base modifican los valores dentro del estado global, mientras que otras consumen esos datos. El orden de ejecución aquí es importante: ¡no queremos consumir datos accidentalmente primero, cuando no hay datos para consumir (todavía)! Comprender el flujo de datos cuando se utiliza un estado global puede resultar muy complicado a medida que la aplicación crece y docenas de componentes dependen unos de otros.
Gestión de estados en React
En React, a menudo confiamos en un estado global a través de herramientas de administración de estado como Redux o React Context en lugar de usar Singletons. Aunque su comportamiento de estado global puede parecer similar al de Singleton, estas herramientas proporcionan un estado de solo lectura en lugar del estado mutable de Singleton. Cuando se usa Redux, solo los reductores de funciones puras pueden actualizar el estado, después de que un componente haya enviado una acción a través de un despachador .
Aunque las desventajas de tener un estado global no desaparecen mágicamente al usar estas herramientas, al menos podemos asegurarnos de que el estado global cambie de la manera que pretendemos, ya que los componentes no pueden actualizar el estado directamente.