Patrón singleton en nodejs: ¿es necesario?

95

Recientemente encontré este artículo sobre cómo escribir un singleton en Node.js. Conozco la documentación de require estados que:

Los módulos se almacenan en caché después de la primera vez que se cargan. Es posible que varias llamadas a require('foo')no hagan que el código del módulo se ejecute varias veces.

Por lo tanto, parece que todos los módulos necesarios se pueden usar fácilmente como singleton sin el código repetitivo singleton.

Pregunta:

¿El artículo anterior proporciona una solución completa para crear un singleton?

mkoryak
fuente
1
Aquí hay un 5 min. explicación sobre este tema (escrito después de v6 y npm3): medium.com/@lazlojuly/…
lazlojuly

Respuestas:

56

Esto tiene básicamente que ver con el almacenamiento en caché de nodejs. Simple y llanamente.

https://nodejs.org/api/modules.html#modules_caching

(v 6.3.1)

Almacenamiento en caché

Los módulos se almacenan en caché después de la primera vez que se cargan. Esto significa (entre otras cosas) que cada llamada a require ('foo') obtendrá exactamente el mismo objeto devuelto, si se resuelve en el mismo archivo.

Es posible que varias llamadas a require ('foo') no hagan que el código del módulo se ejecute varias veces. Esta es una característica importante. Con él, se pueden devolver objetos "parcialmente hechos", lo que permite que se carguen las dependencias transitivas incluso cuando causarían ciclos.

Si desea que un módulo ejecute código varias veces, exporte una función y llame a esa función.

Advertencias sobre el almacenamiento en caché del módulo

Los módulos se almacenan en caché en función de su nombre de archivo resuelto. Dado que los módulos pueden resolverse en un nombre de archivo diferente según la ubicación del módulo de llamada (cargando desde carpetas de node_modules), no es una garantía de que require ('foo') siempre devolverá exactamente el mismo objeto, si se resolviera en archivos diferentes .

Además, en sistemas de archivos o sistemas operativos que no distinguen entre mayúsculas y minúsculas, diferentes nombres de archivo resueltos pueden apuntar al mismo archivo, pero la caché los tratará como módulos diferentes y volverá a cargar el archivo varias veces. Por ejemplo, require ('./ foo') y require ('./ FOO') devuelven dos objetos diferentes, independientemente de si ./foo y ./FOO son el mismo archivo o no.

Así que en términos simples.

Si quieres un Singleton; exportar un objeto .

Si no quieres un Singleton; exportar una función (y hacer cosas / devolver cosas / lo que sea en esa función).

Para ser MUY claro, si lo hace correctamente, debería funcionar, consulte https://stackoverflow.com/a/33746703/1137669 (la respuesta de Allen Luce). Explica en código lo que sucede cuando el almacenamiento en caché falla debido a nombres de archivo resueltos de manera diferente. Pero si SIEMPRE resuelve con el mismo nombre de archivo, debería funcionar.

Actualización 2016

creando un verdadero singleton en node.js con símbolos es6 Otra solución : en este enlace

Actualización 2020

Esta respuesta se refiere a CommonJS (la forma propia de Node.js para importar / exportar módulos). Es muy probable que Node.js cambie a los módulos ECMAScript : https://nodejs.org/api/esm.html (ECMAScript es el nombre real de JavaScript si no lo sabía)

Al migrar a ECMAScript, lea lo siguiente por ahora: https://nodejs.org/api/esm.html#esm_writing_dual_packages_ while_avoiding_or_minimizing_hazards

K - La toxicidad en SO está aumentando.
fuente
3
Si quieres un Singleton; exportar un objeto ... que ayudó gracias
danday74
1
Esta es una especie de mala idea, por muchas razones que se dan en otra parte de esta página, pero el concepto es esencialmente válido, lo que quiere decir que, bajo las circunstancias nominales establecidas, las afirmaciones de esta respuesta son verdaderas. Si desea un singleton rápido y sucio, es probable que esto funcione, simplemente no inicie ningún transbordador con el código.
Adam Tolley
@AdamTolley "por muchas razones dadas en otra parte de esta página", ¿se refiere a archivos de enlace simbólico o nombres de archivos mal escritos que aparentemente no utilizan la misma caché? Sí establece en la documentación el problema con respecto a los sistemas de archivos o sistemas operativos que no distinguen entre mayúsculas y minúsculas. Con respecto al enlace simbólico, puede leer más aquí, ya que se discutió en github.com/nodejs/node/issues/3402 . Además, si está enlazando archivos o no comprende su sistema operativo y su nodo correctamente, entonces no debería estar cerca de la industria de la ingeniería aeroespacial;), sin embargo, entiendo su punto ^^.
K - La toxicidad en SO está aumentando.
@KarlMorrison - solo por el hecho de que la documentación no lo garantiza, el hecho de que parece ser un comportamiento no especificado, o cualquier otra razón racional para no confiar en este comportamiento particular del lenguaje. Tal vez el caché funciona de manera diferente en otra implementación, o le gusta trabajar en REPL y subvertir la función de almacenamiento en caché por completo. Mi punto es que la caché es un detalle de implementación, y su uso como equivalente singleton es un truco inteligente. Me encanta cortes inteligentes, sino que se deben diferenciar, eso es todo - (tampoco nadie está lanzando un servicio de enlace con el nodo, que estaba siendo tonta)
Adam Tolley
136

Todo lo anterior es demasiado complicado. Hay una escuela de pensamiento que dice que los patrones de diseño muestran deficiencias del lenguaje real.

Los lenguajes con POO basada en prototipos (sin clase) no necesitan un patrón singleton en absoluto. Simplemente crea un objeto único (tonelada) sobre la marcha y luego lo usa.

En cuanto a los módulos en el nodo, sí, de forma predeterminada se almacenan en caché, pero se puede modificar, por ejemplo, si desea cargar en caliente los cambios del módulo.

Pero sí, si desea utilizar un objeto compartido en todas partes, ponerlo en un módulo de exportación está bien. Simplemente no lo complique con el "patrón singleton", no es necesario en JavaScript.


fuente
27
Es extraño que nadie obtenga votos positivos ... tenga un +1 paraThere is a school of thought which says design patterns are showing deficiencies of actual language.
Esailija
64
Los singleton no son un anti-patrón.
wprl
4
@herby, parece una definición demasiado específica (y por lo tanto incorrecta) del patrón singleton.
wprl
19
La documentación dice: "Es posible que varias llamadas a require ('foo') no hagan que el código del módulo se ejecute varias veces". Dice "puede que no", no dice "no lo haré", por lo que preguntar cómo asegurarse de que la instancia del módulo se cree solo una vez en una aplicación es una pregunta válida desde mi punto de vista.
xorcus
9
Es engañoso que esta sea la respuesta correcta para esta pregunta. Como @mike señaló a continuación, es posible que un módulo se cargue más de una vez y tenga dos instancias. Estoy teniendo ese problema en el que solo tengo una copia de Knockout pero se crean dos instancias porque el módulo se carga dos veces.
dgaviola
26

No. Cuando falla el almacenamiento en caché del módulo de Node, ese patrón singleton falla. Modifiqué el ejemplo para que se ejecute de manera significativa en OSX:

var sg = require("./singleton.js");
var sg2 = require("./singleton.js");
sg.add(1, "test");
sg2.add(2, "test2");

console.log(sg.getSocketList(), sg2.getSocketList());

Esto da el resultado que el autor anticipó:

{ '1': 'test', '2': 'test2' } { '1': 'test', '2': 'test2' }

Pero una pequeña modificación derrota al almacenamiento en caché. En OSX, haz esto:

var sg = require("./singleton.js");
var sg2 = require("./SINGLETON.js");
sg.add(1, "test");
sg2.add(2, "test2");

console.log(sg.getSocketList(), sg2.getSocketList());

O, en Linux:

% ln singleton.js singleton2.js

Luego cambie la sg2línea requerida a:

var sg2 = require("./singleton2.js");

Y bam , el singleton es derrotado:

{ '1': 'test' } { '2': 'test2' }

No conozco una forma aceptable de solucionar esto. Si realmente siente la necesidad de hacer algo parecido a un singleton y está de acuerdo con contaminar el espacio de nombres global (y los muchos problemas que pueden resultar), puede cambiar las líneas getInstance()y del autor exportsa:

singleton.getInstance = function(){
  if(global.singleton_instance === undefined)
    global.singleton_instance = new singleton();
  return global.singleton_instance;
}

module.exports = singleton.getInstance();

Dicho esto, nunca me he encontrado con una situación en un sistema de producción en la que necesite hacer algo como esto. Tampoco he sentido nunca la necesidad de utilizar el patrón singleton en Javascript.

Allen Luce
fuente
21

Mirando un poco más en las advertencias de almacenamiento en caché del módulo en los documentos de los módulos:

Los módulos se almacenan en caché en función de su nombre de archivo resuelto. Dado que los módulos pueden resolverse en un nombre de archivo diferente según la ubicación del módulo de llamada (cargando desde carpetas de node_modules), no es una garantía que require ('foo') siempre devolverá exactamente el mismo objeto, si se resolviera en archivos diferentes .

Entonces, dependiendo de dónde se encuentre cuando necesite un módulo, es posible obtener una instancia diferente del módulo.

Parece que los módulos no son una solución simple para crear singletons.

Editar: O tal vez lo sean . Como @mkoryak, no puedo pensar en un caso en el que un solo archivo pueda resolverse en diferentes nombres de archivo (sin usar enlaces simbólicos). Pero (como comenta @JohnnyHK), se node_modulescargarán y almacenarán por separado varias copias de un archivo en diferentes directorios.

Miguel
fuente
ok, lo leí 3 veces y todavía no puedo pensar en un ejemplo en el que se resolvería con un nombre de archivo diferente. ¿ayuda?
mkoryak
1
@mkoryak Creo que se está refiriendo a casos en los que tienes dos módulos diferentes que necesitas y node_modulesdonde cada uno depende del mismo módulo, pero hay copias separadas de ese módulo dependiente bajo el node_modulessubdirectorio de cada uno de los dos módulos diferentes.
JohnnyHK
@mike, estás aquí, ese módulo se instancia varias veces cuando se hace referencia a través de diferentes rutas. Me acerqué al caso al escribir pruebas unitarias para los módulos del servidor. Necesito una especie de instancia singleton. como lograrlo
Sushil
Un ejemplo podrían ser las rutas relativas. p.ej. Dado que se require('./db')encuentra en dos archivos separados, el código del dbmódulo se ejecuta dos veces
willscripted el
9
Acabo de tener un error desagradable ya que el sistema de módulo de nodo no distingue entre mayúsculas y minúsculas. Llamé require('../lib/myModule.js');en un archivo y require('../lib/mymodule.js');en otro y no entregó el mismo objeto.
heyarne
18

Un singleton en node.js (o en el navegador JS, para el caso) como ese es completamente innecesario.

Dado que los módulos se almacenan en caché y tienen estado, el ejemplo que se proporciona en el enlace que proporcionó podría reescribirse fácilmente de manera mucho más simple:

var socketList = {};

exports.add = function (userId, socket) {
    if (!socketList[userId]) {
        socketList[userId] = socket;
    }
};

exports.remove = function (userId) {
    delete socketList[userId];
};

exports.getSocketList = function () {
    return socketList;
};
// or
// exports.socketList = socketList
Paul Armstrong
fuente
5
Los documentos dicen que " no puede hacer que el código del módulo se ejecute varias veces", por lo que es posible que se llame varias veces, y si este código se ejecuta de nuevo, socketList se restablecerá a una lista vacía
Jonathan.
3
@Jonathan. El contexto en los documentos alrededor de esa cita parece ser un caso bastante convincente que puede que no se use en un estilo RFC NO DEBE .
Michael
6
@Michael "may" es una palabra divertida como esa. ¿Te apetece tener una palabra que cuando se niega significa "quizás no" o "definitivamente no" ..
OJFord
1
Se may notaplica cuando npm linkotros módulos durante el desarrollo. Por lo tanto, tenga cuidado al usar módulos que se basan en una sola instancia, como eventBus.
mediafreakch
10

La única respuesta aquí que usa clases ES6

// SummaryModule.js
class Summary {

  init(summary) {
    this.summary = summary
  }

  anotherMethod() {
    // do something
  }
}

module.exports = new Summary()

requieren este singleton con:

const summary = require('./SummaryModule')
summary.init(true)
summary.anotherMethod()

El único problema aquí es que no puede pasar parámetros al constructor de la clase, pero eso puede evitarse llamando manualmente a un initmétodo.

danday74
fuente
la pregunta es "se necesitan singletons", no "cómo se escribe uno"
mkoryak
@ danday74 ¿Cómo podemos usar la misma instancia summaryen otra clase, sin inicializarla nuevamente?
M Faisal Hameed
1
En Node.js solo requícelo en otro archivo ... const summary = require ('./ SummaryModule') ... y será la misma instancia. Puede probar esto creando una variable miembro y estableciendo su valor en un archivo que lo requiera y luego obteniendo su valor en otro archivo que lo requiera. Debe ser el valor que se estableció.
danday74
9

No necesita nada especial para hacer un singleton en js, el código del artículo también podría ser:

var socketList = {};

module.exports = {
      add: function() {

      },

      ...
};

Fuera de node.js (por ejemplo, en el navegador js), debe agregar la función de contenedor manualmente (se hace automáticamente en node.js):

var singleton = function() {
    var socketList = {};
    return {
        add: function() {},
        ...
    };
}();
Esailija
fuente
Como lo señaló @Allen Luce, si el almacenamiento en caché del nodo falla, el patrón singleton también falla.
rakeen el
6

Los singleton están bien en JS, simplemente no necesitan ser tan detallados.

En el nodo, si necesita un singleton, por ejemplo, para usar la misma instancia de ORM / DB en varios archivos en su capa de servidor, puede incluir la referencia en una variable global.

Simplemente escriba un módulo que cree la var global si no existe, luego devuelva una referencia a eso.

@ allen-luce lo hizo bien con su ejemplo de código de nota al pie copiado aquí:

singleton.getInstance = function(){
  if(global.singleton_instance === undefined)
    global.singleton_instance = new singleton();
  return global.singleton_instance;
};

module.exports = singleton.getInstance();

pero es importante tener en cuenta que nonew es necesario utilizar la palabra clave . Cualquier objeto antiguo, función, vida, etc. funcionará; aquí no hay vudú OOP.

puntos de bonificación si cierra algún obj dentro de una función que devuelve una referencia a él, y hace que esa función sea global, incluso la reasignación de la variable global no afectará las instancias ya creadas a partir de ella, aunque esto es cuestionablemente útil.

Adam Tolley
fuente
no necesitas nada de eso. puede hacerlo module.exports = new Foo()porque module.exports no se ejecutará nuevamente, a menos que haga algo realmente estúpido
mkoryak
Absolutamente NO debe confiar en los efectos secundarios de la implementación. Si necesita una sola instancia, simplemente conéctela a una global, en caso de que cambie la implementación.
Adam Tolley
La respuesta anterior también fue un malentendido de la pregunta original como '¿Debería usar singletons en JS o el lenguaje los hace innecesarios?', Que también parece ser un problema con muchas de las otras respuestas. Respaldo mi recomendación en contra del uso de la implementación require como reemplazo de una implementación única explícita y adecuada.
Adam Tolley
1

Manteniéndolo simple.

foo.js

function foo() {

  bar: {
    doSomething: function(arg, callback) {
      return callback('Echo ' + arg);
    };
  }

  return bar;
};

module.exports = foo();

Entonces solo

var foo = require(__dirname + 'foo');
foo.doSomething('Hello', function(result){ console.log(result); });
Comer en Joes
fuente
la pregunta es "se necesitan singletons", no "cómo se escribe uno"
mkoryak