Padrão Singleton em nodejs - é necessário?

95

Recentemente encontrei este artigo sobre como escrever um singleton em Node.js. Conheço a documentação dos require estados que:

Os módulos são armazenados em cache após a primeira vez que são carregados. Várias chamadas para require('foo')não podem fazer com que o código do módulo seja executado várias vezes.

Portanto, parece que cada módulo necessário pode ser facilmente usado como um singleton sem o código-clichê de singleton.

Questão:

O artigo acima fornece uma solução geral para a criação de um singleton?

mkoryak
fonte
1
Aqui está um 5 min. explicação sobre este tópico (escrita após v6 e npm3): medium.com/@lazlojuly/…
lazlojuly

Respostas:

56

Isso tem basicamente a ver com o cache do nodejs. Claro e simples.

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

(v 6.3.1)

Cache

Os módulos são armazenados em cache após a primeira vez que são carregados. Isso significa (entre outras coisas) que cada chamada para require ('foo') obterá exatamente o mesmo objeto retornado, se resolver para o mesmo arquivo.

Múltiplas chamadas para require ('foo') podem não fazer com que o código do módulo seja executado várias vezes. Este é um recurso importante. Com ele, objetos "parcialmente concluídos" podem ser retornados, permitindo que dependências transitivas sejam carregadas mesmo quando causariam ciclos.

Se você quiser que um módulo execute o código várias vezes, exporte uma função e chame essa função.

Advertências de cache de módulo

Os módulos são armazenados em cache com base em seu nome de arquivo resolvido. Uma vez que os módulos podem resolver para um nome de arquivo diferente com base na localização do módulo de chamada (carregando das pastas node_modules), não é uma garantia que require ('foo') sempre retornará exatamente o mesmo objeto, se resolver para arquivos diferentes .

Além disso, em sistemas de arquivos ou sistemas operacionais que não diferenciam maiúsculas de minúsculas, diferentes nomes de arquivos resolvidos podem apontar para o mesmo arquivo, mas o cache ainda os tratará como módulos diferentes e recarregará o arquivo várias vezes. Por exemplo, require ('./ foo') e require ('./ FOO') retornam dois objetos diferentes, independentemente de ./foo e ./FOO serem ou não o mesmo arquivo.

Então, em termos simples.

Se você quer um Singleton; exportar um objeto .

Se você não quer um Singleton; exportar uma função (e fazer coisas / retornar coisas / qualquer coisa nessa função).

Para ser MUITO claro, se você fizer isso corretamente, deve funcionar, consulte https://stackoverflow.com/a/33746703/1137669 (resposta de Allen Luce). Ele explica no código o que acontece quando o cache falha devido a nomes de arquivo resolvidos de forma diferente. Mas se você SEMPRE resolver para o mesmo nome de arquivo, ele deve funcionar.

Atualização 2016

criando um verdadeiro singleton em node.js com símbolos es6 Outra solução : neste link

Atualização 2020

Esta resposta se refere a CommonJS (a maneira própria do Node.js para importar / exportar módulos). O Node.js provavelmente mudará para os Módulos ECMAScript : https://nodejs.org/api/esm.html (ECMAScript é o nome real do JavaScript, caso você não saiba)

Ao migrar para ECMAScript, leia o seguinte por enquanto: https://nodejs.org/api/esm.html#esm_writing_dual_packages_while_avoiding_or_minimizing_hazards

K - A toxicidade em SO está crescendo.
fonte
3
Se você quer um Singleton; exportar um objeto ... que ajudou, obrigado
danday74
1
Esta é uma ideia meio ruim - por muitas razões apresentadas em outra parte desta página - mas o conceito é essencialmente válido, o que significa que, nas circunstâncias nominais estabelecidas, as afirmações nesta resposta são verdadeiras. Se você quiser um singleton rápido e sujo, provavelmente funcionará - apenas não lance nenhum ônibus espacial com o código.
Adam Tolley
@AdamTolley "por muitas razões fornecidas em outra parte desta página", você está se referindo a arquivos de symlinking ou erros de ortografia de nomes de arquivo que aparentemente não utilizam o mesmo cache? Ele declara na documentação o problema em relação aos sistemas de arquivos ou sistemas operacionais que não diferenciam maiúsculas de minúsculas. Com relação a links simbólicos, você pode ler mais aqui conforme discutido em github.com/nodejs/node/issues/3402 . Além disso, se você está vinculando arquivos simbolicamente ou não entende seu sistema operacional e nó corretamente, então você não deveria estar em qualquer lugar perto da indústria de engenharia aeroespacial;), no entanto entendo seu ponto ^^.
K - A toxicidade em SO está crescendo.
@KarlMorrison - apenas pelo fato de a documentação não o garantir, pelo fato de parecer um comportamento não especificado, ou qualquer outra razão racional para não confiar neste comportamento particular da linguagem. Talvez o cache funcione de forma diferente em outra implementação, ou você gosta de trabalhar em REPLs e subverter o recurso de cache por completo. Meu ponto é que o cache é um detalhe de implementação, e seu uso como equivalente singleton é um hack inteligente. Eu amo hacks inteligentes, mas eles devem ser diferenciados, só isso - (também ninguém está lançando naves com node, eu estava sendo bobo)
Adam Tolley
136

Todas as opções acima são complicadas demais. Há uma escola de pensamento que diz que os padrões de projeto estão mostrando deficiências da linguagem real.

Linguagens com OOP baseado em protótipo (sem classes) não precisam de um padrão singleton. Você simplesmente cria um único objeto (tonelada) na hora e, em seguida, o usa.

Quanto aos módulos no nó, sim, por padrão eles são armazenados em cache, mas pode ser ajustado, por exemplo, se você quiser o carregamento a quente das alterações do módulo.

Mas sim, se você quiser usar o objeto compartilhado todo, colocá-lo em um módulo de exportação é bom. Só não complique com "padrão singleton", não há necessidade dele em JavaScript.


fonte
27
É estranho que ninguém esteja recebendo There is a school of thought which says design patterns are showing deficiencies of actual language.
votos positivos
64
Singletons não são um antipadrão.
wprl
4
@herby, parece uma definição excessivamente específica (e, portanto, incorreta) do padrão singleton.
wprl
19
A documentação diz: "Múltiplas chamadas para require ('foo') podem não fazer com que o código do módulo seja executado várias vezes.". Diz "não posso", não diz "não vou", portanto, perguntar como garantir que a instância do módulo seja criada apenas uma vez em um aplicativo é uma questão válida do meu ponto de vista.
xorcus
9
É enganoso que esta seja a resposta correta para esta pergunta. Como @mike apontou abaixo, é possível que um módulo seja carregado mais de uma vez e você tenha duas instâncias. Estou encontrando aquele problema em que tenho apenas uma cópia do Knockout, mas duas instâncias são criadas porque o módulo é carregado duas vezes.
dgaviola
26

Não. Quando o cache do módulo do Node falha, esse padrão singleton falha. Modifiquei o exemplo para executar significativamente no 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());

Isso dá a saída que o autor antecipou:

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

Mas uma pequena modificação anula o cache. No OSX, faça o seguinte:

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

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

Ou, no Linux:

% ln singleton.js singleton2.js

Em seguida, altere a sg2linha necessária para:

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

E bam , o singleton é derrotado:

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

Não sei de uma maneira aceitável de contornar isso. Se você realmente sente a necessidade de fazer algo parecido com um singleton e não tem problema em poluir o namespace global (e os muitos problemas que podem resultar), você pode alterar o autor getInstance()e as exportslinhas para:

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

module.exports = singleton.getInstance();

Dito isso, nunca me deparei com uma situação em um sistema de produção em que precisasse fazer algo assim. Também nunca senti a necessidade de usar o padrão singleton em Javascript.

Allen Luce
fonte
21

Olhando um pouco mais adiante, nas advertências de cache do módulo nos documentos dos módulos:

Os módulos são armazenados em cache com base em seu nome de arquivo resolvido. Uma vez que os módulos podem resolver para um nome de arquivo diferente com base na localização do módulo de chamada (carregando das pastas node_modules), não é uma garantia que require ('foo') sempre retornará exatamente o mesmo objeto, se resolver para arquivos diferentes .

Portanto, dependendo de onde você está quando precisa de um módulo, é possível obter uma instância diferente do módulo.

Parece que os módulos não são uma solução simples para a criação de singletons.

Edit: Ou talvez sejam . Como @mkoryak, não consigo pensar em um caso em que um único arquivo possa resolver para nomes de arquivo diferentes (sem usar links simbólicos). Mas (como comentários de @JohnnyHK), várias cópias de um arquivo em node_modulesdiretórios diferentes serão carregadas e armazenadas separadamente.

Mike
fonte
ok, eu li isso 3 vezes e ainda não consigo pensar em um exemplo em que resolveria para um nome de arquivo diferente. Socorro?
mkoryak 01 de
1
@mkoryak Acho que se refere a casos em que você tem dois módulos diferentes exigidos, dos node_modulesquais cada um depende do mesmo módulo, mas há cópias separadas desse módulo dependente no node_modulessubdiretório de cada um dos dois módulos diferentes.
JohnnyHK 01 de
@mike você está bem aqui que o módulo é instanciado várias vezes quando referenciado por caminhos diferentes. Eu acertei o caso ao escrever testes de unidade para os módulos de servidor. Eu preciso de uma espécie de instância singleton. como conseguir isso?
Sushil de
Um exemplo pode ser caminhos relativos. por exemplo. Dado require('./db')está em dois arquivos separados, o código para o dbmódulo é executado duas vezes
willscripted
9
Acabei de ter um bug desagradável, pois o sistema de módulo de nó é insenstivie caso. Liguei require('../lib/myModule.js');em um arquivo e require('../lib/mymodule.js');em outro e não entregou o mesmo objeto.
heyarne,
18

Um singleton no node.js (ou no navegador JS, nesse caso) como esse é completamente desnecessário.

Como os módulos são armazenados em cache e com estado, o exemplo fornecido no link que você forneceu poderia ser facilmente reescrito de forma muito mais simples:

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
fonte
5
Os documentos dizem " não pode fazer com que o código do módulo seja executado várias vezes", então é possível que ele seja chamado várias vezes, e se esse código for executado novamente, o socketList será redefinido para uma lista vazia
Jonathan.
3
@Jonathan. O contexto nos documentos em torno dessa citação parece ser um caso bastante convincente que pode não estar sendo usado em um estilo RFC NÃO DEVE .
Michael,
6
@Michael "pode" é uma palavra engraçada como essa. Imagine ter uma palavra que, quando negada, significa "talvez não" ou "definitivamente não".
OJFord
1
Isso may notse aplica quando você npm linkoutros módulos durante o desenvolvimento. Portanto, tome cuidado ao usar módulos que dependem de uma única instância, como um eventBus.
mediafreakch
10

A única resposta aqui que usa classes ES6

// SummaryModule.js
class Summary {

  init(summary) {
    this.summary = summary
  }

  anotherMethod() {
    // do something
  }
}

module.exports = new Summary()

requer este singleton com:

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

O único problema aqui é que você não pode passar parâmetros para o construtor da classe, mas isso pode ser contornado chamando manualmente um initmétodo.

danday74
fonte
a questão é "são necessários singletons", não "como você escreve um"
mkoryak
@ danday74 Como podemos usar a mesma instância summaryem outra classe, sem inicializá-la novamente?
M Faisal Hameed de
1
Em Node.js apenas solicite em outro arquivo ... const summary = require ('./ SummaryModule') ... e será a mesma instância. Você pode testar isso criando uma variável de membro e definindo seu valor em um arquivo que o exija e, em seguida, obtendo seu valor em outro arquivo que o exija. Deve ser o valor definido.
danday74 de
9

Você não precisa de nada especial para fazer um singleton em js, o código no artigo também poderia ser:

var socketList = {};

module.exports = {
      add: function() {

      },

      ...
};

Fora de node.js (por exemplo, no navegador js), você precisa adicionar a função de wrapper manualmente (isso é feito automaticamente em node.js):

var singleton = function() {
    var socketList = {};
    return {
        add: function() {},
        ...
    };
}();
Esailija
fonte
Como apontado por @Allen Luce, se o cache do nó falhar, o padrão do singleton também falhará.
rakeen
6

Singletons são bons em JS, mas não precisam ser tão prolixos.

No nó, se você precisar de um singleton, por exemplo, para usar a mesma instância ORM / DB em vários arquivos em sua camada de servidor, você pode colocar a referência em uma variável global.

Basta escrever um módulo que cria a var global, se ela não existir, e retorna uma referência a ela.

@ allen-luce acertou com seu exemplo de código de nota de rodapé copiado aqui:

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

module.exports = singleton.getInstance();

mas é importante notar que usar a newpalavra-chave não é obrigatório. Qualquer objeto, função, vida, etc. antigo funcionará - não há vodu OOP acontecendo aqui.

pontos de bônus se você fechar um algum obj dentro de uma função que retorna uma referência a ele e tornar essa função global - mesmo a reatribuição da variável global não afetará as instâncias já criadas a partir dela - embora isso seja questionavelmente útil.

Adam Tolley
fonte
você não precisa de nada disso. você pode simplesmente fazer module.exports = new Foo()porque module.exports não será executado novamente, a menos que você faça algo realmente estúpido
mkoryak
Você absolutamente NÃO deve confiar nos efeitos colaterais da implementação. Se precisar de uma única instância, basta vinculá-la a uma global, caso a implementação seja alterada.
Adam Tolley
A resposta acima também foi um mal-entendido da pergunta original como 'Devo usar singletons em JS ou a linguagem os torna desnecessários?', O que também parece ser um problema com muitas das outras respostas. Eu mantenho minha recomendação contra o uso da implementação require como um substituto para uma implementação singleton apropriada e explícita.
Adam Tolley
1

Mantendo as coisas simples.

foo.js

function foo() {

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

  return bar;
};

module.exports = foo();

Então só

var foo = require(__dirname + 'foo');
foo.doSomething('Hello', function(result){ console.log(result); });
Comer no Joes
fonte
a questão é "são necessários singletons", não "como você escreve um"
mkoryak