Mô hình Singleton trong nodejs - nó có cần thiết không?


95

Gần đây tôi đã xem qua bài viết này về cách viết một singleton trong Node.js. Tôi biết tài liệu về các require tiểu bang rằng:

Các mô-đun được lưu vào bộ nhớ đệm sau lần đầu tiên chúng được tải. Nhiều lệnh gọi đến require('foo')có thể không làm cho mã mô-đun được thực thi nhiều lần.

Vì vậy, có vẻ như mọi mô-đun được yêu cầu đều có thể dễ dàng được sử dụng như một singleton mà không cần mã boilerplate singleton.

Câu hỏi:

Bài viết trên có cung cấp một vòng về giải pháp tạo singleton không?


1
Đây là 5 phút. giải thích về chủ đề này (được viết sau v6 và npm3): medium.com/@lazlojuly/…
lazlojuly

Câu trả lời:


56

Điều này về cơ bản liên quan đến bộ nhớ đệm của nodejs. Thông thường và đơn giản.

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

(v 6.3.1)

Bộ nhớ đệm

Các mô-đun được lưu vào bộ nhớ đệm sau lần đầu tiên chúng được tải. Điều này có nghĩa là (trong số những thứ khác) rằng mọi lệnh gọi yêu cầu ('foo') sẽ nhận được chính xác cùng một đối tượng được trả về, nếu nó phân giải thành cùng một tệp.

Nhiều lệnh gọi yêu cầu ('foo') có thể không khiến mã mô-đun được thực thi nhiều lần. Đây là một tính năng quan trọng. Với nó, các đối tượng "đã hoàn thành một phần" có thể được trả về, do đó cho phép tải các phụ thuộc bắc cầu ngay cả khi chúng gây ra chu kỳ.

Nếu bạn muốn có một mô-đun thực thi mã nhiều lần, thì hãy xuất một hàm và gọi hàm đó.

Mô-đun Caching cảnh báo

Các mô-đun được lưu vào bộ nhớ đệm dựa trên tên tệp đã phân giải của chúng. Vì mô-đun có thể phân giải thành một tên tệp khác dựa trên vị trí của mô-đun đang gọi (tải từ các thư mục node_modules), nên không đảm bảo rằng request ('foo') sẽ luôn trả về cùng một đối tượng, nếu nó phân giải thành các tệp khác nhau .

Ngoài ra, trên các hệ điều hành hoặc hệ thống tệp không phân biệt chữ hoa chữ thường, các tên tệp đã phân giải khác nhau có thể trỏ đến cùng một tệp, nhưng bộ đệm ẩn sẽ vẫn coi chúng là các mô-đun khác nhau và sẽ tải lại tệp nhiều lần. Ví dụ: Request ('./ foo') và Required ('./ FOO') trả về hai đối tượng khác nhau, bất kể ./foo và ./FOO có phải là cùng một tệp hay không.

Vì vậy, trong điều kiện đơn giản.

Nếu bạn muốn một Singleton; xuất một đối tượng .

Nếu bạn không muốn một Singleton; xuất một hàm (và thực hiện công cụ / trả lại nội dung / bất cứ điều gì trong hàm đó).

Để RẤT rõ ràng, nếu bạn làm đúng cách, nó sẽ hoạt động, hãy xem https://stackoverflow.com/a/33746703/1137669 (câu trả lời của Allen Luce). Nó giải thích trong mã điều gì sẽ xảy ra khi bộ nhớ đệm không thành công do các tên tệp được phân giải khác nhau. Nhưng nếu bạn LUÔN giải quyết cho cùng một tên tệp, nó sẽ hoạt động.

Cập nhật 2016

tạo một singleton thực sự trong node.js với các ký hiệu es6 Một giải pháp khác : trong liên kết này

Cập nhật 2020

Câu trả lời này đề cập đến CommonJS (theo cách riêng của Node.js của các mô-đun nhập khẩu / xuất khẩu). Node.js rất có thể sẽ chuyển sang Mô-đun ECMAScript : https://nodejs.org/api/esm.html (ECMAScript là tên thật của JavaScript nếu bạn không biết)

Khi di chuyển sang ECMAScript, hãy đọc phần sau: https://nodejs.org/api/esm.html#esm_writing_dual_packages_ately_avoiding_or_minimizing_hazards


3
Nếu bạn muốn một Singleton; xuất khẩu một đối tượng ... đã giúp nhờ
danday74

1
Đây là một ý tưởng tồi - vì nhiều lý do được đưa ra ở những nơi khác trên trang này - nhưng khái niệm này về cơ bản có giá trị, nghĩa là trong các trường hợp danh nghĩa đã được thiết lập, các tuyên bố trong câu trả lời này là đúng. Nếu bạn muốn có một singleton nhanh và bẩn, điều này có thể sẽ hoạt động - chỉ cần không khởi chạy bất kỳ con thoi nào với mã.
Adam Tolley

@AdamTolley "vì nhiều lý do được đưa ra ở những nơi khác trên trang này", bạn đang nói đến các tệp liên kết tượng trưng hoặc tên tệp sai chính tả dường như không sử dụng cùng một bộ nhớ cache? Nó nêu rõ trong tài liệu vấn đề liên quan đến hệ thống tệp hoặc hệ điều hành không phân biệt chữ hoa chữ thường. Về liên kết tượng trưng, ​​bạn có thể đọc thêm ở đây vì nó đã được thảo luận github.com/nodejs/node/issues/3402 . Ngoài ra, nếu bạn đang liên kết tệp hoặc không hiểu đúng hệ điều hành và nút của mình thì bạn không nên ở bất kỳ đâu gần ngành kỹ thuật hàng không vũ trụ;), tuy nhiên, tôi hiểu ý bạn ^^.
K - Độc tính trong SO ngày càng lớn.

@KarlMorrison - thực tế là tài liệu không đảm bảo điều đó, thực tế là nó có vẻ là hành vi không xác định hoặc bất kỳ lý do hợp lý nào khác cho việc không tin tưởng hành vi cụ thể này của ngôn ngữ. Có thể bộ nhớ cache hoạt động khác trong một triển khai khác, hoặc bạn thích làm việc trong REPL và phá hủy hoàn toàn tính năng bộ nhớ đệm. Quan điểm của tôi là bộ nhớ cache là một chi tiết triển khai, và việc sử dụng nó như một phần tử tương đương là một cách hack thông minh. Tôi yêu hacks thông minh, nhưng họ nên được phân biệt, đó là tất cả - (cũng không có ai sẽ ra mắt tàu con thoi với nút, tôi đã là ngớ ngẩn)
Adam Tolley

136

Tất cả những điều trên là quá phức tạp. Có một trường phái tư tưởng nói rằng các mẫu thiết kế đang thể hiện sự thiếu sót của ngôn ngữ thực tế.

Các ngôn ngữ với OOP dựa trên nguyên mẫu (không phân lớp) hoàn toàn không cần một mẫu singleton. Bạn chỉ cần tạo một đối tượng (tấn) một cách nhanh chóng và sau đó sử dụng nó.

Đối với các mô-đun trong nút, có, theo mặc định chúng được lưu vào bộ nhớ đệm, nhưng nó có thể được điều chỉnh, ví dụ như nếu bạn muốn tải nóng các thay đổi của mô-đun.

Nhưng có, nếu bạn muốn sử dụng đối tượng chia sẻ trên toàn bộ, đặt nó trong một xuất khẩu mô-đun là tốt. Chỉ cần đừng làm phức tạp nó với "singleton pattern", không cần nó trong JavaScript.


27
Đó là nhận upvotes không ai lạ của ... có 1 choThere is a school of thought which says design patterns are showing deficiencies of actual language.
Esailija

64
Singletons không phải là một kiểu chống đối.
wprl

4
@herby, có vẻ như là một định nghĩa quá cụ thể (và do đó không chính xác) về mẫu singleton.
wprl

19
Tài liệu viết: "Nhiều lệnh gọi yêu cầu ('foo') có thể không khiến mã mô-đun được thực thi nhiều lần.". Nó nói "có thể không", nó không nói "sẽ không", vì vậy hỏi làm thế nào để đảm bảo phiên bản mô-đun chỉ được tạo một lần trong một ứng dụng là một câu hỏi hợp lệ theo quan điểm của tôi.
xorcus

9
Nó là sai lầm rằng đây là câu trả lời chính xác cho câu hỏi này. Như @mike đã chỉ ra bên dưới, có thể một mô-đun được tải nhiều lần và bạn có hai trường hợp. Tôi đang gặp phải vấn đề đó là tôi chỉ có một bản sao của Knockout nhưng hai phiên bản được tạo vì mô-đun được tải hai lần.
dgaviola

26

Không. Khi bộ nhớ đệm mô-đun của Node bị lỗi, mẫu singleton đó không thành công. Tôi đã sửa đổi ví dụ để chạy có ý nghĩa trên 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());

Điều này cho kết quả mà tác giả dự đoán:

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

Nhưng một sửa đổi nhỏ sẽ đánh bại bộ nhớ đệm. Trên OSX, hãy làm như sau:

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

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

Hoặc trên Linux:

% ln singleton.js singleton2.js

Sau đó, thay đổi sg2dòng yêu cầu thành:

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

bam , singleton đã bị đánh bại:

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

Tôi không biết một cách có thể chấp nhận được để giải quyết vấn đề này. Nếu bạn thực sự cảm thấy cần phải tạo ra một cái gì đó giống như singleton và đồng ý với việc làm ô nhiễm không gian tên chung (và nhiều vấn đề có thể dẫn đến), bạn có thể thay đổi tác giả getInstance()exportsdòng thành:

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

module.exports = singleton.getInstance();

Điều đó nói rằng, tôi chưa bao giờ gặp phải tình huống trong một hệ thống sản xuất mà tôi cần phải làm bất cứ điều gì như thế này. Tôi cũng chưa bao giờ cảm thấy cần phải sử dụng mẫu singleton trong Javascript.


21

Tìm hiểu thêm một chút về Thông báo lưu vào bộ đệm ẩn của Mô-đun trong tài liệu Mô-đun:

Các mô-đun được lưu vào bộ nhớ đệm dựa trên tên tệp đã phân giải của chúng. Vì mô-đun có thể phân giải thành một tên tệp khác dựa trên vị trí của mô-đun đang gọi (tải từ các thư mục node_modules), nên không đảm bảo rằng request ('foo') sẽ luôn trả về cùng một đối tượng, nếu nó phân giải thành các tệp khác nhau .

Vì vậy, tùy thuộc vào vị trí của bạn khi bạn yêu cầu một mô-đun, bạn có thể nhận được một phiên bản khác của mô-đun.

Có vẻ như các mô-đun không phải là một giải pháp đơn giản để tạo các đĩa đơn.

Chỉnh sửa: Hoặc có thể họ . Giống như @mkoryak, tôi không thể đưa ra trường hợp một tệp có thể phân giải thành các tên tệp khác nhau (mà không sử dụng liên kết tượng trưng). Nhưng (như @JohnnyHK nhận xét), nhiều bản sao của một tệp trong các node_modulesthư mục khác nhau sẽ được tải và lưu trữ riêng biệt.


ok, tôi đã đọc nó 3 lần và tôi vẫn không thể nghĩ về một ví dụ mà nó sẽ giải quyết thành một tên tệp khác. Cứu giúp?
mkoryak

1
@mkoryak Tôi nghĩ đó là đề cập đến các trường hợp bạn có hai mô-đun khác nhau mà bạn đang yêu cầu trong node_modulesđó mỗi mô-đun phụ thuộc vào cùng một mô-đun, nhưng có các bản sao riêng biệt của mô-đun phụ thuộc đó trong node_modulesthư mục con của mỗi trong số hai mô-đun khác nhau.
JohnnyHK

@mike bạn đang ở đây mà mô-đun được khởi tạo nhiều lần khi được tham chiếu qua các đường dẫn khác nhau. Tôi gặp phải trường hợp này khi viết các bài kiểm tra đơn vị cho các mô-đun máy chủ. Tôi cần loại cá thể singleton. làm thế nào để đạt được nó?
Sushil

Một ví dụ có thể là các đường dẫn tương đối. ví dụ. Đã cho require('./db')nằm trong hai tệp riêng biệt, mã cho dbmô-đun thực thi hai lần
willscripted

9
Tôi vừa gặp một lỗi khó chịu vì hệ thống mô-đun nút là phân biệt chữ hoa chữ thường. tôi đã gọi require('../lib/myModule.js');trong tệp này và tệp require('../lib/mymodule.js');khác và nó không phân phối cùng một đối tượng.
heyarne

18

Một singleton trong node.js (hoặc trong JS của trình duyệt) như vậy là hoàn toàn không cần thiết.

Vì các mô-đun được lưu trong bộ nhớ cache và có trạng thái, ví dụ được đưa ra trên liên kết bạn đã cung cấp có thể dễ dàng được viết lại đơn giản hơn nhiều:

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
Các tài liệu nói rằng " có thể không khiến mã mô-đun được thực thi nhiều lần", vì vậy có thể nó sẽ được gọi nhiều lần và nếu mã này được thực thi lại, socketList sẽ được đặt lại thành danh sách trống
Jonathan.

3
@Jonathan. Bối cảnh trong các tài liệu xung quanh câu trích dẫn đó dường như tạo ra một trường hợp khá thuyết phục mà có thể KHÔNG ĐƯỢC sử dụng theo kiểu RFC .
Michael

6
@Michael "may" là một từ hài hước như thế. Thật kỳ lạ khi có một từ mà khi bị phủ định có nghĩa là "có lẽ không" hoặc "chắc chắn là không" ..
OJFord

1
Áp may notdụng khi bạn npm linkcác mô-đun khác trong quá trình phát triển. Vì vậy, hãy cẩn thận khi sử dụng các mô-đun dựa trên một phiên bản đơn lẻ chẳng hạn như eventBus.
mediafreakch

10

Câu trả lời duy nhất ở đây sử dụng các lớp ES6

// SummaryModule.js
class Summary {

  init(summary) {
    this.summary = summary
  }

  anotherMethod() {
    // do something
  }
}

module.exports = new Summary()

yêu cầu singleton này với:

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

Vấn đề duy nhất ở đây là bạn không thể chuyển các tham số cho hàm tạo lớp nhưng điều đó có thể bị phá vỡ bằng cách gọi thủ công một initphương thức.


câu hỏi là "đang độc thân cần thiết", không phải "làm thế nào để bạn viết một"
mkoryak

@ danday74 Làm thế nào chúng ta có thể sử dụng cùng một cá thể summarytrong một lớp khác mà không cần khởi tạo lại nó?
M Faisal Hameed

1
Trong Node.js chỉ cần yêu cầu nó trong một tệp khác ... const Summary = request ('./ SummaryModule') ... và nó sẽ giống như vậy. Bạn có thể kiểm tra điều này bằng cách tạo một biến thành viên và đặt giá trị của nó trong một tệp yêu cầu nó và sau đó lấy giá trị của nó trong một tệp khác yêu cầu. Nó phải là giá trị đã được đặt.
danday74

9

Bạn không cần bất cứ điều gì đặc biệt để thực hiện một singleton trong js, mã trong bài viết cũng có thể là:

var socketList = {};

module.exports = {
      add: function() {

      },

      ...
};

Bên ngoài node.js (ví dụ: trong js của trình duyệt), bạn cần thêm hàm wrapper theo cách thủ công (nó được thực hiện tự động trong node.js):

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

Như đã chỉ ra bởi @Allen Luce, nếu bộ nhớ đệm của nút không thành công thì mẫu singleton cũng không thành công.
rakeen

6

Singleton trong JS cũng được, chỉ cần chúng không dài dòng như vậy.

Trong nút, nếu bạn cần một singleton, chẳng hạn như để sử dụng cùng một phiên bản ORM / DB trên các tệp khác nhau trong lớp máy chủ của mình, bạn có thể nhồi tham chiếu vào một biến toàn cục.

Chỉ cần viết một mô-đun tạo var toàn cục nếu nó không tồn tại, sau đó trả về một tham chiếu đến đó.

@ allen-luce đã làm đúng với ví dụ mã chú thích cuối trang của anh ấy được sao chép ở đây:

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

module.exports = singleton.getInstance();

nhưng điều quan trọng cần lưu ý là sử dụng newtừ khóa là không bắt buộc. Mọi đối tượng, chức năng cũ, iife, v.v. sẽ hoạt động - không có hành vi sai trái nào xảy ra ở đây.

điểm thưởng nếu bạn đóng một số đối tượng bên trong một hàm trả về tham chiếu đến nó và biến hàm đó thành toàn cục - thì ngay cả việc gán lại biến toàn cục cũng sẽ không cản trở các thể hiện đã được tạo từ nó - mặc dù điều này rất hữu ích.


bạn không cần bất kỳ điều đó. bạn chỉ có thể làm module.exports = new Foo()vì module.exports sẽ không thực hiện một lần nữa, trừ khi bạn làm điều gì đó thực sự ngu ngốc
mkoryak

Bạn tuyệt đối KHÔNG nên tin tưởng vào tác dụng phụ khi thực hiện. Nếu bạn cần một phiên bản duy nhất, chỉ cần liên kết nó với một toàn cầu, trong trường hợp việc triển khai thay đổi.
Adam Tolley

Câu trả lời ở trên cũng là một sự hiểu lầm của câu hỏi ban đầu là 'Tôi có nên sử dụng các singlelet trong JS hay ngôn ngữ khiến chúng trở nên không cần thiết?', Đây có vẻ là một vấn đề với rất nhiều câu trả lời khác. Tôi ủng hộ khuyến nghị của mình không nên sử dụng triển khai request để thay thế cho việc triển khai singleton rõ ràng, thích hợp.
Adam Tolley

1

Giữ nó đơn giản.

foo.js

function foo() {

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

  return bar;
};

module.exports = foo();

Sau đó chỉ

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

câu hỏi là "đang độc thân cần thiết", không phải "làm thế nào để bạn viết một"
mkoryak
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.