รูปแบบ Singleton ใน nodejs - จำเป็นหรือไม่?


97

ฉันเพิ่งเจอบทความนี้เกี่ยวกับวิธีการเขียนซิงเกิลตันใน Node.js ฉันรู้เอกสารของrequire รัฐที่:

โมดูลจะถูกแคชไว้หลังจากโหลดครั้งแรก การโทรหลายครั้งrequire('foo')อาจไม่ทำให้โค้ดโมดูลถูกเรียกใช้งานหลายครั้ง

ดังนั้นดูเหมือนว่าทุกโมดูลที่จำเป็นสามารถใช้เป็นซิงเกิลตันได้อย่างง่ายดายโดยไม่ต้องใช้รหัสสำเร็จรูปแบบซิงเกิลตัน

คำถาม:

บทความข้างต้นให้ข้อมูลเกี่ยวกับวิธีการสร้างซิงเกิลตันหรือไม่?


1
นี่คือ 5 นาที คำอธิบายในหัวข้อนี้ (เขียนหลัง v6 และ npm3): medium.com/@lazlojuly/…
lazlojuly

คำตอบ:


60

สิ่งนี้เกี่ยวข้องกับการแคช nodejs โดยทั่วไป เรียบง่าย.

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

(v 6.3.1)

เก็บเอาไว้

โมดูลจะถูกแคชไว้หลังจากโหลดครั้งแรก ซึ่งหมายความว่า (เหนือสิ่งอื่นใด) ทุกการเรียกที่ต้องการ ('foo') จะได้รับอ็อบเจ็กต์เดียวกันทั้งหมดที่ส่งคืนหากจะแก้ไขเป็นไฟล์เดียวกัน

การเรียกหลายครั้งที่ต้องการ ('foo') อาจไม่ทำให้โค้ดโมดูลถูกเรียกใช้งานหลายครั้ง นี่คือคุณสมบัติที่สำคัญ ด้วยมันสามารถส่งคืนอ็อบเจ็กต์ "เสร็จสิ้นบางส่วน" ได้ซึ่งทำให้สามารถโหลดการอ้างอิงสกรรมกริยาได้แม้ว่าจะทำให้เกิดรอบ

หากคุณต้องการให้โมดูลรันโค้ดหลายครั้งให้ส่งออกฟังก์ชันและเรียกใช้ฟังก์ชันนั้น

คำเตือนการแคชโมดูล

โมดูลจะถูกแคชตามชื่อไฟล์ที่แก้ไข เนื่องจากโมดูลอาจแก้ไขเป็นชื่อไฟล์อื่นตามตำแหน่งของโมดูลการเรียกใช้ (การโหลดจากโฟลเดอร์ node_modules) จึงไม่รับประกันว่าต้องการให้ ('foo') จะส่งคืนอ็อบเจ็กต์เดียวกันทุกครั้งหากจะแก้ไขเป็นไฟล์อื่น .

นอกจากนี้ในระบบไฟล์หรือระบบปฏิบัติการที่ไม่คำนึงถึงตัวพิมพ์เล็กและใหญ่ชื่อไฟล์ที่แก้ไขต่างกันสามารถชี้ไปที่ไฟล์เดียวกันได้ แต่แคชจะยังถือว่าเป็นโมดูลที่แตกต่างกันและจะโหลดไฟล์ซ้ำหลายครั้ง ตัวอย่างเช่นต้องใช้ ('./ foo') และกำหนดให้ ('./ FOO') ส่งคืนอ็อบเจ็กต์สองรายการที่แตกต่างกันไม่ว่า. / foo และ ./FOO จะเป็นไฟล์เดียวกันหรือไม่

พูดง่ายๆก็คือ

ถ้าคุณต้องการ Singleton; ส่งออกวัตถุ

หากคุณไม่ต้องการ Singleton ส่งออกฟังก์ชัน (และทำสิ่งของ / ส่งคืนสิ่งของ / อะไรก็ตามในฟังก์ชันนั้น)

เพื่อความชัดเจนมากหากคุณทำอย่างถูกต้องควรได้ผลโปรดดูที่https://stackoverflow.com/a/33746703/1137669 (คำตอบของ Allen Luce) มันอธิบายในโค้ดว่าจะเกิดอะไรขึ้นเมื่อการแคชล้มเหลวเนื่องจากชื่อไฟล์ที่แก้ไขต่างกัน แต่ถ้าคุณมักจะแก้ไขเป็นชื่อไฟล์เดียวกันควรใช้งานได้

อัปเดต 2016

การสร้าง singleton ที่แท้จริงใน 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
ถ้าคุณต้องการ Singleton; ส่งออกวัตถุ ... ที่ช่วยขอบคุณ
danday74

1
นี่เป็นความคิดที่ไม่ดีด้วยเหตุผลหลายประการที่ให้ไว้ที่อื่นในหน้านี้ - แต่แนวคิดนี้ถูกต้องโดยพื้นฐานกล่าวคือภายใต้สถานการณ์ที่กำหนดขึ้นการอ้างสิทธิ์ในคำตอบนี้เป็นความจริง หากคุณต้องการซิงเกิลตันที่รวดเร็วและสกปรกสิ่งนี้น่าจะใช้ได้ - เพียงแค่อย่าเปิดรถรับส่งด้วยรหัส
Adam Tolley

@AdamTolley "ด้วยเหตุผลหลายประการที่ระบุไว้ที่อื่นในหน้านี้" คุณกำลังอ้างถึงไฟล์ symlinking หรือชื่อไฟล์ที่สะกดผิดซึ่งดูเหมือนจะไม่ใช้แคชเดียวกันหรือไม่? มันระบุในเอกสารเกี่ยวกับปัญหาเกี่ยวกับระบบไฟล์หรือระบบปฏิบัติการที่ไม่คำนึงถึงขนาดตัวพิมพ์ เกี่ยวกับ symlinking คุณสามารถอ่านเพิ่มเติมได้ที่นี่ในขณะที่มันถูกกล่าวถึงgithub.com/nodejs/node/issues/3402 นอกจากนี้หากคุณกำลังเชื่อมโยงไฟล์หรือไม่เข้าใจระบบปฏิบัติการและโหนดของคุณอย่างถูกต้องคุณไม่ควรอยู่ใกล้อุตสาหกรรมวิศวกรรมการบินและอวกาศ) อย่างไรก็ตามฉันเข้าใจประเด็นของคุณ ^^
basickarl

@KarlMorrison - สำหรับข้อเท็จจริงที่เอกสารไม่รับประกันความจริงที่ว่าดูเหมือนว่าจะเป็นพฤติกรรมที่ไม่ระบุรายละเอียดหรือเหตุผลอื่นใดที่ไม่เชื่อถือพฤติกรรมเฉพาะของภาษานี้ บางทีแคชอาจทำงานแตกต่างกันในการใช้งานอื่นหรือคุณชอบทำงานใน REPL และล้มล้างคุณลักษณะการแคชทั้งหมด ประเด็นของฉันคือแคชเป็นรายละเอียดการใช้งานและการใช้งานเทียบเท่ากับซิงเกิลตันเป็นการแฮ็กที่ชาญฉลาด ฉันชอบแฮ็คที่ฉลาด แต่มันควรจะแตกต่างนั่นคือทั้งหมด - (ยังไม่มีใครเปิดตัวรถรับส่งด้วยโหนดฉันก็โง่)
Adam Tolley

138

ทั้งหมดที่กล่าวมามีความซับซ้อนมากเกินไป มีโรงเรียนแห่งความคิดที่บอกว่ารูปแบบการออกแบบแสดงข้อบกพร่องของภาษาจริง

ภาษาที่มี OOP ตามต้นแบบ (ไม่มีคลาส) ไม่จำเป็นต้องมีรูปแบบซิงเกิลตันเลย คุณเพียงแค่สร้างวัตถุ (ตัน) ชิ้นเดียวได้ทันทีแล้วใช้มัน

สำหรับโมดูลในโหนดใช่โดยค่าเริ่มต้นจะมีการแคช แต่สามารถปรับแต่งได้เช่นหากคุณต้องการโหลดการเปลี่ยนแปลงโมดูลอย่างร้อนแรง

แต่ใช่ถ้าคุณต้องการใช้วัตถุที่ใช้ร่วมกันทั้งหมดการวางไว้ในการส่งออกโมดูลก็ใช้ได้ อย่าซับซ้อนด้วย "รูปแบบซิงเกิลตัน" ไม่จำเป็นต้องใช้ใน JavaScript


27
มันแปลกที่ไม่มีใครได้รับการโหวตเพิ่มขึ้น ... มี +1 สำหรับThere is a school of thought which says design patterns are showing deficiencies of actual language.
Esailija

64
เสื้อกล้ามไม่ได้ต่อต้านรูปแบบ
wprl

5
@herby ดูเหมือนคำจำกัดความเฉพาะเจาะจงมากเกินไป (และไม่ถูกต้อง) ของรูปแบบซิงเกิลตัน
wprl

20
เอกสารประกอบระบุว่า: "การเรียกหลายครั้งที่ต้องการ ('foo') อาจไม่ทำให้โค้ดโมดูลถูกเรียกใช้งานหลายครั้ง" มันบอกว่า "อาจไม่" ไม่ใช่ว่า "จะไม่" ดังนั้นการถามว่าจะสร้างอินสแตนซ์โมดูลเพียงครั้งเดียวในแอปพลิเคชันได้อย่างไรจึงเป็นคำถามที่ถูกต้องจากมุมมองของฉัน
xorcus

9
ทำให้เข้าใจผิดว่านี่คือคำตอบที่ถูกต้องสำหรับคำถามนี้ ดังที่ @mike ระบุไว้ด้านล่างเป็นไปได้ว่าโมดูลจะโหลดมากกว่าหนึ่งครั้งและคุณมีสองอินสแตนซ์ ฉันประสบปัญหาที่ว่าฉันมี Knockout เพียงสำเนาเดียว แต่มีการสร้างสองอินสแตนซ์เนื่องจากโมดูลโหลดสองครั้ง
dgaviola

26

ไม่ เมื่อการแคชโมดูลของโหนดล้มเหลวรูปแบบซิงเกิลตันนั้นจะล้มเหลว ฉันแก้ไขตัวอย่างเพื่อให้ทำงานอย่างมีความหมายบน 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

ดูเพิ่มเติมอีกเล็กน้อยที่Module Caching Caveatsในเอกสาร Modules:

โมดูลจะถูกแคชตามชื่อไฟล์ที่แก้ไข เนื่องจากโมดูลอาจแก้ไขเป็นชื่อไฟล์อื่นตามตำแหน่งของโมดูลการเรียกใช้ (การโหลดจากโฟลเดอร์ node_modules) จึงไม่รับประกันว่าต้องการให้ ('foo') จะส่งคืนอ็อบเจ็กต์เดียวกันทุกครั้งหากจะแก้ไขเป็นไฟล์อื่น .

ดังนั้นขึ้นอยู่กับว่าคุณอยู่ที่ไหนเมื่อคุณต้องการโมดูลคุณสามารถรับอินสแตนซ์ที่แตกต่างกันของโมดูลได้

ดูเหมือนว่าโมดูลไม่ใช่วิธีง่ายๆในการสร้างเสื้อกล้าม

แก้ไข:หรือบางทีพวกเขามี เช่น @mkoryak ฉันไม่สามารถหากรณีที่ไฟล์เดียวอาจแก้ไขเป็นชื่อไฟล์ที่แตกต่างกัน (โดยไม่ใช้ symlinks) แต่ (ตามความคิดเห็นของ @JohnnyHK) สำเนาไฟล์หลายชุดในnode_modulesไดเรกทอรีต่างๆจะถูกโหลดและจัดเก็บแยกกัน


โอเคฉันอ่าน 3 ครั้งแล้วและยังคิดไม่ออกว่าจะแก้ไขเป็นชื่อไฟล์อื่นได้อย่างไร ช่วยด้วย?
mkoryak

1
@mkoryak ฉันคิดว่านั่นหมายถึงกรณีที่คุณมีโมดูลสองโมดูลที่แตกต่างกันที่คุณต้องการnode_modulesโดยที่แต่ละโมดูลขึ้นอยู่กับโมดูลเดียวกัน แต่มีสำเนาแยกต่างหากของโมดูลที่ขึ้นต่อกันภายใต้node_modulesไดเร็กทอรีย่อยของแต่ละโมดูลที่แตกต่างกันสองโมดูล
JohnnyHK

@mike คุณอยู่ที่นี่โมดูลได้รับการสร้างอินสแตนซ์หลายครั้งเมื่ออ้างอิงผ่านเส้นทางที่แตกต่างกัน ฉันโดนกรณีนี้เมื่อเขียนการทดสอบหน่วยสำหรับโมดูลเซิร์ฟเวอร์ ฉันต้องการอินสแตนซ์ซิงเกิลตัน จะบรรลุได้อย่างไร?
Sushil

ตัวอย่างอาจเป็นเส้นทางสัมพัทธ์ เช่น. ระบุrequire('./db')อยู่ในไฟล์สองไฟล์ที่แยกจากกันรหัสสำหรับdbโมดูลจะดำเนินการสองครั้ง
willscripted

9
ฉันเพิ่งมีข้อผิดพลาดที่น่ารังเกียจเนื่องจากระบบโมดูลโหนดเป็นแบบ case-insenstivie ฉันเรียกrequire('../lib/myModule.js');ในไฟล์เดียวและrequire('../lib/mymodule.js');อีกไฟล์หนึ่งและไม่ได้ส่งมอบวัตถุเดียวกัน
heyarne

19

Singleton ใน 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 สไตล์ไม่ต้อง
Michael

6
@ Michael "อาจ" เป็นคำพูดตลก ๆ แบบนั้น แฟนซีมีคำว่าเมื่อปฏิเสธแปลว่า "อาจจะไม่ใช่" หรือ "ไม่แน่นอน" ..
OJFord

1
may notใช้เมื่อคุณnpm linkโมดูลอื่น ๆ ในระหว่างการพัฒนา ดังนั้นโปรดใช้ความระมัดระวังเมื่อใช้โมดูลที่ใช้อินสแตนซ์เดียวเช่น eventBus
mediafreakch

11

คุณไม่ต้องการอะไรเป็นพิเศษในการทำ singleton ใน js โค้ดในบทความอาจเป็น:

var socketList = {};

module.exports = {
      add: function() {

      },

      ...
};

ภายนอก node.js (เช่นในเบราว์เซอร์ js) คุณต้องเพิ่มฟังก์ชัน wrapper ด้วยตนเอง (จะทำโดยอัตโนมัติใน node.js):

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

ตามที่ระบุไว้โดย @Allen Luce หากการแคชของโหนดล้มเหลวรูปแบบซิงเกิลตันก็ล้มเหลวเช่นกัน
rakeen

10

คำตอบเดียวที่นี่ที่ใช้คลาส ES6

// SummaryModule.js
class Summary {

  init(summary) {
    this.summary = summary
  }

  anotherMethod() {
    // do something
  }
}

module.exports = new Summary()

ต้องการ singleton นี้ด้วย:

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

ปัญหาเดียวที่นี่คือคุณไม่สามารถส่งพารามิเตอร์ไปยังตัวสร้างคลาสได้ แต่สามารถหลีกเลี่ยงได้โดยการเรียกใช้initเมธอดด้วยตนเอง


คำถามคือ "ต้องมีเสื้อกล้าม" ไม่ใช่ "คุณเขียนอย่างไร"
mkoryak

@ danday74 เราจะใช้อินสแตนซ์เดียวกันsummaryในคลาสอื่นได้อย่างไรโดยไม่ต้องเริ่มต้นอีกครั้ง
M Faisal Hameed

1
ใน Node.js ต้องการเพียงแค่ในไฟล์อื่น ... const summary = ต้องใช้ ('./ SummaryModule') ... และจะเป็นอินสแตนซ์เดียวกัน คุณสามารถทดสอบได้โดยสร้างตัวแปรสมาชิกและตั้งค่าในไฟล์เดียวที่ต้องการจากนั้นรับค่าในไฟล์อื่นที่ต้องการ ควรเป็นค่าที่ตั้งไว้
danday74

6

Singletons นั้นใช้ได้ดีใน JS พวกเขาไม่จำเป็นต้องละเอียดมากนัก

ในโหนดหากคุณต้องการซิงเกิลตันตัวอย่างเช่นในการใช้อินสแตนซ์ ORM / DB เดียวกันกับไฟล์ต่างๆในเลเยอร์เซิร์ฟเวอร์ของคุณคุณสามารถใส่ข้อมูลอ้างอิงลงในตัวแปรส่วนกลางได้

เพียงแค่เขียนโมดูลที่สร้าง global 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 ฯลฯ จะใช้งานได้ - ไม่มี OOP วูดูเกิดขึ้นที่นี่

คะแนนโบนัสหากคุณปิด obj บางส่วนภายในฟังก์ชันที่ส่งคืนการอ้างอิงไปยังฟังก์ชันนั้นและทำให้ฟังก์ชันนั้นเป็นแบบโกลบอล - จากนั้นการกำหนดตัวแปรโกลบอลใหม่จะไม่ทำให้อินสแตนซ์ที่สร้างขึ้นจากอินสแตนซ์ที่สร้างขึ้นมาแล้ว - แม้ว่าจะมีประโยชน์อย่างน่าสงสัยก็ตาม


คุณไม่ต้องการสิ่งนั้น คุณสามารถทำได้module.exports = new Foo()เพราะ module.exports จะไม่ดำเนินการอีกเว้นแต่คุณจะทำอะไรโง่ ๆ
mkoryak

คุณไม่ควรพึ่งพาผลข้างเคียงของการใช้งานโดยเด็ดขาด หากคุณต้องการอินสแตนซ์เดียวเพียงผูกอินสแตนซ์กับโกลบอลในกรณีที่การใช้งานเปลี่ยนแปลง
Adam Tolley

คำตอบข้างต้นยังเป็นความเข้าใจผิดของคำถามเดิมว่า 'ฉันควรใช้ singletons ใน JS หรือไม่หรือภาษาทำให้ไม่จำเป็น?' ซึ่งดูเหมือนจะเป็นปัญหากับคำตอบอื่น ๆ อีกมากมาย ฉันปฏิบัติตามคำแนะนำของฉันที่ไม่ให้ใช้การใช้งานที่จำเป็นเพื่อทดแทนการใช้งานซิงเกิลตันที่เหมาะสมและชัดเจน
Adam Tolley

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); });

คำถามคือ "ต้องมีเสื้อกล้าม" ไม่ใช่ "คุณเขียนอย่างไร"
mkoryak
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.