Node.js 需要外部输入的单例模块模式

Node.js singleton module pattern that requires external inputs

通常我们可以创建一个像这样的简单单例对象 Node.js:

var foo = {};

module.exports = foo;

function Foo(){}

module.exports = new Foo();

然而

制作需要外部变量进行初始化的干净单例模块的​​最佳方法是什么?我最终做了这样的事情:

var value = null;

function init(val){

 if(value === null){
    value = val;
  }

  return value;
}

module.exports = init;

这样,使用该模块的人就可以为某个变量传入一个初始化值。另一种方法是这样的:

function Baz(value){
this.value = value;
}

var instance = null;

module.exports = function init(value){

if(instance === null){
    instance = new Baz(value);
}
   return instance;

}

我遇到了两个问题:

(1) 这个是次要的,但是语义是错误的。我们可以将 init 重命名为 getInstance,但我们不能使相同的函数字面意思为 "initialize and get",因为它们是不同的含义。所以我们必须有一个函数来做两件不同的事情。创建实例并检索实例。我特别不喜欢这样,因为在某些情况下我们需要确保初始化实例的参数不为空。对于多个开发人员使用一个模块,不清楚模块是否已初始化,如果他们将 undefined 传递给尚未初始化的模块,这可能会成为一个问题,或者至少会造成混淆。

(2) 这更重要——在某些情况下初始化 Baz 是异步的。例如,建立 Redis 连接或从文件读取以初始化单例,或建立 socket.io 连接。这才是真正让我失望的地方。

例如这是我有一个我认为非常丑陋的模块,它存储了一个 socket.io 连接:

    var io = null;

    var init = function ($io) {

        if (io === null) {

            io = $io;

            io.on('connection', function (socket) {

                socket.on('disconnect', function () {

                });

            });
        }

        return io;
    };

module.exports = {
    getSocketIOConn: init
};

上面的模块是这样初始化的:

var server = http.createServer(app);
var io = socketio.listen(server);
require('../controllers/socketio.js').getSocketIOConn(io);

所以我正在寻找一种设计模式,它允许我们创建一个初始化过程是异步的单例模块。理想情况下,我们不会使用相同的函数来初始化实例和检索实例。有这种东西吗?

我认为不一定有一种方法可以创建解决此问题的模式,但也许我错误地以某种方式构建我的代码,这会产生一个不需要存在的问题-仅使用一个值初始化模块一次,但使用一个函数来初始化实例和检索实例的问题。

听起来您正在尝试创建一个在一个地方初始化的模块,然后将该初始化中的一些共享资源用于该模块的其他用户。这是现实世界中半常见的需求。

首先,如果一个模块可以加载或创建它所依赖的东西,那是最理想的,因为这使得它本身更加模块化和有用,并且减少了使用它的人的负担。因此,在您的情况下,如果您的模块可以 create/load 首次创建模块时需要的东西,并将该资源存储在它自己的模块变量中,那么这将是理想的情况。但是,这并不总是可能的,因为共享资源可能是其他人的责任来设置和初始化,而这个模块只需要知道这一点。

因此,通常的做法是只使用模块的构造函数。在 Javascript 中,您可以允许构造函数采用提供初始化信息的可选参数。负责设置模块的代码将使用所需的设置参数调用构造函数。模块的其他不负责设置模块的用户可以不调用构造函数,或者如果他们想要 return 值或者他们应该传递其他构造函数参数,他们可以传递 null 该设置参数。

例如,您可以这样做:

var io;

module.exports = function(setup_io) {
    if (setup_io) {
        io = setup_io;
    }
    return module.exports;
};

module.exports.method1 = function() {
    if (!io) {
        throw new Error("Can't use method1 until io is properly initalized");
    }
    // code here for method1
};

// other methods here

然后,该模块的用户可以这样做:

// load myModule and initialize it with a shared variable
var myModule = require('myModule')(io);

或者这个:

// load myModule without initializing it 
// (assume some other module will initialize it properly)
var myModule = require('myModule');

注意:为了开发人员的理智,拥有需要适当设置(在它们可以被正确使用之前)的单独方法以在调用任何需要该设置的方法时检查模块是否已设置会很有用为了在正确设置模块之前正确地通知开发人员他们已经调用了一个方法。否则,错误可能会发生在下游更远的地方,并且可能不会有有用的错误消息。


如果您现在希望初始化过程是异步的,那也可以做到,但这肯定会使模块的其他用途复杂化,因为他们不一定知道 when/if 模块已经初始化。

var moduleData;
var readyList = new EventEmitter();

module.exports = function(arg, callback) {
    // do some async operation here involving arg
    // when that operation completes, you stored the result
    // in local module data and call the callback
    readyList.on("ready", callback);
    someAsyncOperation(arg, function() {
        // set moduleData here
        // notify everyone else that the module is now ready
        readyList.emit("ready");
        // remove all listeners since this is a one-shot event
        readyList.removeAllListeners("ready");
    });
    return module.exports;
};

如果您有此模块的其他用户希望在完成初始化时收到通知,您可以允许他们自己注册一个回调,以便在模块准备就绪时收到通知。

// pass a callback to this method that will be called
// async when the module is ready
module.exports.ready = function(fn) {
    // if module already ready, then schedule the callback immediately
    if (moduleData) {
        setImmediate(fn);
    } else {
        readyList.on("ready", fn);
    }
};

如果出于我不太明白的原因,你想对初始化和就绪检测使用相同的构造函数,那是可以做到的,尽管我认为它不像使用单独的构造函数那么清楚就绪检测方法:

var moduleData;
var readyList = new EventEmitter();

module.exports = function(arg, callback) {
    // if both arguments passed, assume this is a request for module
    // initialization
    if (arguments.length === 2) {
        // do some async operation here involving arg
        // when that operation completes, you stored the result
        // in local module data and call the callback
        readyList.on("ready", callback);
        someAsyncOperation(arg, function() {
            // set moduleData here
            // notify everyone else that the module is now ready
            readyList.emit("ready");
            // remove all listeners since this is a one-shot event
            readyList.removeAllListeners("ready");
        });
    } else {
        // constructor called just for a ready request
        // arg is the callback
        if (moduleData) {
            // if module already ready, then schedule the callback immediately
            setImmediate(arg);
        } else {
            // otherwise, save the callback
            readyList.on("ready", arg);
        }
    }
    return module.exports;
};

异步初始化模块的用法:

// async initialization form
var myModule = require("myModule")(someArg, function() {
    // can use myModule here
});

加载模块并在其他人初始化时收到通知的用法:

var myModule = require("myModule")(function() {
    // can use myModule here
});