Patrones de Diseño: Patrón singleton (Página 2)

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 la countervariable
  • un método increment que incrementa el valor de counteren uno
  • un método decrement que disminuye el valor de counteren 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 getInstancemé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 instanceigual 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 Counterejemplo. Disponemos de los siguientes archivos:

  • counter.js: contiene la clase Counter y exporta una instancia **Counter como su exportación predeterminada
  • index.js: carga los módulos redButton.js y blueButton.js
  • redButton.js: importa Counter y agrega Counter el método increment como detector de eventos al botón rojo y registra el valor actual de counter invocando el método getCount
  • blueButton.js: importa Counter y agrega Counterel incrementmétodo como detector de eventos al botón azul y registra el valor actual de counter invocando el método getCount

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 instanceen 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, counteres simplemente un objeto que contiene:

  • una countpropiedad
  • un incrementmétodo que incrementa el valor de counten uno
  • un decrementmétodo que disminuye el valor de counten uno

Dado que los objetos se pasan por referencia, ambos redButton.jsy blueButton.jsimportan una referencia al mismo counterobjeto. Modificar el valor de counten 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.jsen este caso, puede que no sea obvio que el módulo está importando un Singleton. En otros archivos, como index.jsen 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 lety constevita 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 modulesistema en JavaScript facilita la creación de valores accesibles globalmente sin contaminar el alcance global, al poder almacenar exportvalores de un módulo y importesos 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.


Relacionado

Libro de Patrones en Diseño de Aplicaciones Web (libro GRATIS en inglés)

Patterns.dev es un libro gratuito sobre patrones de diseño y patrones de componentes para crear aplicaciones web potentes con JavaScript básico y React. DESCARGAR Patrones de Aprendizaje - Patrones para crear aplicaciones web potentes con JavaScript básico y React (libro en inglés) (traducción española en proceso..) ¡SEGUIR LEYENDO!

Patrones de Diseño: Introducción (Página 1)

Los patrones de diseño son una parte fundamental del desarrollo de software, ya que proporcionan soluciones típicas a problemas comúnmente recurrentes en el diseño de software. En lugar de proporcionar piezas de software específicas, los patrones de diseño son simplemente conceptos que pueden usarse para manejar ¡SEGUIR LEYENDO!

Patrones de Diseño: Patrón de Proxy (Página 3)

Con un objeto Proxy, tenemos más control sobre las interacciones con ciertos objetos. Un objeto proxy puede determinar el comportamiento cada vez que interactuamos con el objeto, por ejemplo cuando obtenemos un valor o establecemos un valor. En términos generales, un apoderado significa un sustituto de ¡SEGUIR LEYENDO!

Patrones de Diseño: Patrón de Proveedor (Página 4)

En algunos casos, queremos que los datos estén disponibles para muchos (si no todos) los componentes de una aplicación. Aunque podemos pasar datos a los componentes usando props, esto puede resultar difícil si casi todos los componentes de su aplicación necesitan acceder al valor de los ¡SEGUIR LEYENDO!