Шаблон синглтона в nodejs - чи потрібен він?


95

Нещодавно я натрапив на цю статтю про те, як написати синглтон в Node.js. Я знаю документацію require держав, яка:

Модулі кешуються після першого завантаження. Кілька викликів до require('foo')не можуть спричинити багаторазове виконання коду модуля.

Отже, здається, що кожен необхідний модуль може бути легко використаний як синглтон без єдиного шаблонного коду.

Питання:

Чи наведена вище стаття пропонує навколо рішення щодо створення синглтона?


1
Ось 5 хв. Пояснення на цю тему (написане після v6 та npm3): medium.com/@lazlojuly/…
lazlojuly

Відповіді:


56

В основному це пов’язано з кешуванням nodejs. Простий і простий.

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

(v 6.3.1)

Кешування

Модулі кешуються після першого завантаження. Це означає (серед іншого), що при кожному виклику, щоб вимагати ('foo'), повертатиметься точно такий самий об'єкт, якщо він буде вирішуватись у тому самому файлі.

Кілька викликів, щоб вимагати ('foo'), можуть не спричиняти багаторазове виконання коду модуля. Це важлива особливість. За допомогою нього можна повернути "частково зроблені" об'єкти, що дозволяє завантажувати транзитивні залежності навіть тоді, коли вони спричинять цикли.

Якщо ви хочете, щоб модуль виконував код кілька разів, експортуйте функцію та викличте цю функцію.

Модуль кешування застережень

Модулі кешуються на основі їх дозволеного імені файлу. Оскільки модулі можуть вирішувати інше ім'я файлу залежно від місця розташування викличного модуля (завантаження з папок node_modules), це не гарантія, що вимагає ('foo') завжди повертати такий самий об'єкт, якщо він вирішить різні файли .

Крім того, у файлових системах, що не враховують регістр, або операційних системах, різні вирішені назви файлів можуть вказувати на один і той самий файл, але кеш все одно буде розглядати їх як різні модулі і перезавантажуватиме файл кілька разів. Наприклад, require ('./ foo') і require ('./ FOO') повертають два різні об'єкти, незалежно від того, є ./foo і ./FOO однаковим файлом.

Тож простими словами.

Якщо ви хочете синглтона; експортувати об’єкт .

Якщо ви не хочете синглтона; експортувати функцію (і робити речі / повертати речі / будь-що в цій функції).

Щоб бути ДУЖЕ зрозумілим, якщо ви зробите це належним чином, це повинно працювати, перегляньте https://stackoverflow.com/a/33746703/1137669 (відповідь Аллена Люса). Це пояснює в коді, що відбувається, коли кешування не вдається через різне вирішення імен файлів. Але якщо Ви ЗАВЖДИ вирішите одне і те ж ім'я файлу, це повинно працювати.

Оновлення 2016 року

створення справжнього синглтона в node.js із символами es6 Інше рішення : за цим посиланням

Оновлення 2020 року

Ця відповідь стосується CommonJS (власний спосіб імпорту / експорту модулів Node.js). Node.js, швидше за все, перейде на модулі ECMAScript : https://nodejs.org/api/esm.html (ECMAScript - справжнє ім'я JavaScript, якщо ви цього не знали)

При переході до ECMAScript на даний момент прочитайте наступне: https://nodejs.org/api/esm.html#esm_writing_dual_packages_ while_avoiding_or_minimizing_hazards


3
Якщо ви хочете синглтона; експортувати об’єкт ..., що допомогло, дякую
danday74

1
Це якась погана ідея - з багатьох причин, наведених в інших місцях на цій сторінці, - але ця концепція є по суті дійсною, тобто, що за встановлених номінальних обставин твердження у цій відповіді відповідають дійсності. Якщо ви хочете швидкий і брудний синглтон, це, швидше за все, спрацює - просто не запускайте човники з кодом.
Адам Толлі

@AdamTolley "з багатьох причин, наведених в інших місцях на цій сторінці", ви маєте на увазі файли символічного посилання або помилки, що, мабуть, не використовують той самий кеш? У документації він зазначає проблему, що стосується файлових систем чи операційних систем, що не враховують регістр. Щодо синхронізації, ви можете прочитати більше тут, як це було обговорено github.com/nodejs/node/isissue/3402 . Крім того, якщо ви посилаєтесь на файли із символічними посиланнями або не розумієте свою операційну систему і не відповідаєте правильному вузлу, тоді ви не повинні знаходитись поблизу аерокосмічної індустрії;), я, однак, розумію вашу думку ^^.
K - Токсичність при SO зростає.

@KarlMorrison - лише за те, що документація цього не гарантує, той факт, що це здається невизначеною поведінкою, або будь-яка інша раціональна причина недовіри саме цій поведінці мови. Можливо, кеш працює по-іншому в іншій реалізації, або вам подобається працювати в REPL і взагалі підривати функцію кешування. Моя думка полягає в тому, що кеш - це деталь реалізації, і його використання як еквівалента одиночного елемента є розумним хаком. Я люблю розумні хаки, але їх слід диференціювати, ось і все - (також ніхто не запускає човники з вузлом, я був дурним)
Адам Толлі

136

Все перераховане є надто складним. Існує школа думок, яка говорить, що шаблони дизайну демонструють недоліки фактичної мови.

Мови з OOP на основі прототипу (безкласові) взагалі не потребують єдиного шаблону. Ви просто створюєте один (тонну) об’єкт на льоту, а потім використовуєте його.

Що стосується модулів у вузлі, так, за замовчуванням вони кешуються, але їх можна налаштувати, наприклад, якщо ви хочете гаряче завантаження змін модуля.

Але так, якщо ви хочете використовувати спільний об’єкт повсюдно, помістити його в модуль експорту - це нормально. Тільки не ускладнюйте його "одностороннім шаблоном", це не потрібно в JavaScript.


27
Це дивно, що ніхто не отримує голосів ... маємо +1 заThere is a school of thought which says design patterns are showing deficiencies of actual language.
Esailija

64
Одиночні не є анти-шаблоном.
wprl

4
@herby, здається надто конкретним (і, отже, неправильним) визначенням шаблону одиночного.
wprl

19
У документації написано: "Кілька викликів для вимоги ('foo') не можуть спричиняти виконання коду модуля кілька разів.". Він говорить "може не", не говорить "не буде", тому запитання, як переконатися, що екземпляр модуля створений лише один раз у програмі, є правильним питанням з моєї точки зору.
xorcus

9
Оманливим є те, що це правильна відповідь на це питання. Як зазначив @mike нижче, можливо, модуль завантажується більше одного разу, і у вас є два екземпляри. Я вражаю цю проблему, коли у мене є лише одна копія Knockout, але створюються два екземпляри, оскільки модуль завантажується двічі.
dgaviola

26

Ні. Коли кешування модулів Node не вдається, не вдається виконати одиночний шаблон. Я модифікував приклад, щоб він змістовно працював на 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());

Це дає результат, який передбачав автор:

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

Але невелика модифікація перемагає кешування. На 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());

Або в Linux:

% ln singleton.js singleton2.js

Потім змініть sg2рядок вимоги на:

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

І бам , сингл переможений:

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

Я не знаю прийнятного способу обійти це. Якщо ви дійсно відчуваєте необхідність зробити щось подібне до одиночного, і у вас все гаразд із забрудненням глобального простору імен (і багатьох проблем, які можуть виникнути в результаті), ви можете змінити автора getInstance()та exportsрядки на:

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

module.exports = singleton.getInstance();

Це означає, що я ніколи не стикався з ситуацією у виробничій системі, де мені потрібно було робити щось подібне. Я також ніколи не відчував необхідності використовувати однотонний візерунок у Javascript.


21

Ознайомившись трохи далі із застереженнями щодо кешування модулів у документації до модулів:

Модулі кешуються на основі їх дозволеного імені файлу. Оскільки модулі можуть перейти до іншого імені файлу залежно від розташування викликаючого модуля (завантаження з папок node_modules), це не гарантія того, що вимагатиме ('foo') завжди повертатиме той самий об'єкт, якщо це буде вирішувати різні файли .

Отже, залежно від того, де ви знаходитесь, коли вам потрібен модуль, можливо отримати інший примірник модуля.

Звучить, що модулі - це не просте рішення для створення одиночних кнопок.

Редагувати: А може, вони і є . Як і @mkoryak, я не можу придумати, щоб один файл міг перейти до різних імен файлів (без використання символьних посилань). Але (як коментує @JohnnyHK), кілька копій файлу в різних node_modulesкаталогах завантажуватимуться і зберігатимуться окремо.


Гаразд, я прочитав це 3 рази, і досі не можу подумати на прикладі, коли це призведе до іншого імені файлу. допомогти?
mkoryak

1
@mkoryak Я думаю, що це стосується випадків, коли у вас є два різні модулі, які вам потрібні, node_modulesде кожен залежить від одного і того ж модуля, але є окремі копії цього залежного модуля в node_modulesпідкаталозі кожного з двох різних модулів.
JohnnyHK

@mike ви маєте тут рацію, що модуль отримує екземпляри кілька разів при посиланні через різні шляхи. Я вразив випадок, коли писав модульні тести для серверних модулів. Мені потрібен вид екземпляра-одиночки. як цього досягти?
Sushil

Прикладом можуть бути відносні шляхи. напр. Даний require('./db')файл знаходиться у двох окремих файлах, код dbмодуля виконується двічі
написано

9
У мене щойно виникла неприємна помилка, оскільки система модулів вузлів не враховує регістр. я зателефонував як require('../lib/myModule.js');в один файл, так і require('../lib/mymodule.js');в інший, і він не доставив той самий об’єкт.
heyarne

18

Такий сингл в node.js (або в браузері JS), подібний, абсолютно непотрібний.

Оскільки модулі кешовані та мають статус, приклад, наведений за посиланням, яке ви надали, можна легко переписати набагато простіше:

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

5
Документи говорять " може не спричиняти виконання модуля кілька разів", тому можливо, що він буде викликаний кілька разів, і якщо цей код буде запущений знову, socketList буде скинуто до порожнього списку
Джонатан.

3
@ Джонатан. Здається, контекст у документах навколо цієї цитати робить досить переконливий випадок, який, можливо, не використовується, використовується у стилі RFC ПОВИННО НЕ .
Майкл

6
@Michael "may" - смішне таке слово.
Незвично сказати

1
may notЗастосовується при npm linkінших модулів в процесі розробки. Тому будьте обережні, використовуючи модулі, які покладаються на один екземпляр, такий як eventBus.
mediafreakch

10

Єдина відповідь тут - використання класів ES6

// SummaryModule.js
class Summary {

  init(summary) {
    this.summary = summary
  }

  anotherMethod() {
    // do something
  }
}

module.exports = new Summary()

потрібен цей синглтон із:

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

Єдина проблема тут полягає в тому, що ви не можете передавати параметри конструктору класу, але це можна обійти ручним викликом initметоду.


питання "потрібні
одинаки

@ danday74 Як ми можемо використовувати той самий екземпляр summaryв іншому класі, не ініціалізуючи його знову?
M Faisal Hameed

1
У Node.js просто вимагайте його в іншому файлі ... const summary = require ('./ SummaryModule') ... і це буде той самий екземпляр. Ви можете перевірити це, створивши змінну члена і встановивши її значення в одному файлі, який цього вимагає, а потім отримати його значення в іншому файлі, який цього вимагає. Це має бути значення, яке було встановлено.
danday74

9

Вам не потрібно нічого особливого робити синглтон в js, код у статті може так само бути:

var socketList = {};

module.exports = {
      add: function() {

      },

      ...
};

Поза node.js (наприклад, у браузері js) вам потрібно додати функцію обгортки вручну (це робиться автоматично в node.js):

var singleton = function() {
    var socketList = {};
    return {
        add: function() {},
        ...
    };
}();

Як зазначає @Allen Luce, якщо кешування вузла не вдається, однократний шаблон також не вдається.
граблі

6

Одиночні люди добре в JS, їм просто не потрібно бути такими багатослівними.

У вузлі, якщо вам потрібен синглтон, наприклад, щоб використовувати один і той же екземпляр ORM / DB у різних файлах на серверному рівні, ви можете вкласти посилання в глобальну змінну.

Просто напишіть модуль, який створює глобальний var, якщо він не існує, а потім повертає посилання на нього.

@ allen-luce це правильно зробив, скопіювавши тут приклад коду виноски:

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

module.exports = singleton.getInstance();

але важливо зазначити, що використовувати newключове слово не потрібно. Буде працювати будь-який старий об’єкт, функція, iife тощо - тут не відбувається вуду ООП.

бонусні бали, якщо ви закриваєте деякий obj всередині функції, яка повертає посилання на неї, і робите цю функцію глобальною - тоді навіть перепризначення глобальної змінної не призведе до збиття вже створених з неї екземплярів - хоча це сумнівно корисно.


вам нічого з цього не потрібно. ви можете просто зробити, module.exports = new Foo()тому що module.exports не буде виконуватися знову, якщо ви не зробите щось справді дурне
mkoryak

Ви абсолютно НЕ повинні покладатися на побічні ефекти впровадження. Якщо вам потрібен один екземпляр, просто прив’яжіть його до глобального, на випадок, якщо реалізація зміниться.
Адам Толлі,

Наведена вище відповідь також спричинила нерозуміння вихідного запитання: "Чи слід використовувати синглтони в JS, чи мова робить їх непотрібними?", Що, здається, також є проблемою з багатьма іншими відповідями. Я дотримуюсь своєї рекомендації проти використання імплементації в якості заміни для належної, явної односторонньої реалізації.
Адам Толлі

1

Зберігаючи це просто.

foo.js

function foo() {

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

  return bar;
};

module.exports = foo();

Тоді просто

var foo = require(__dirname + 'foo');
foo.doSomething('Hello', function(result){ console.log(result); });

питання в тому, "чи потрібні
одиночки
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.