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

时间:2015-08-07 18:21:13

标签: javascript node.js redis socket.io

通常我们可以使用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,但是我们不能使用相同的函数字面意思"初始化并得到"因为它们有不同的含义。所以我们必须有一个能完成两件事的功能。创建实例并检索和实例。我不喜欢这样,因为在某些情况下我们需要确保初始化实例的参数不为null。由于多个开发人员使用模块,目前还不清楚模块是否已经初始化,如果他们将未定义的模块传递给尚未初始化的模块,那么这可能会成为一个问题,或者至少令人困惑

(2)这一点更为重要 - 在某些情况下,初始化Baz是异步的。例如,进行Redis连接或从文件读取以初始化单例或进行socket.io连接。这真是让我失望的原因。

e.g。这是一个我认为非常难看的模块,它存储了一个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);

所以我正在寻找一种设计模式,它允许我们创建一个单例模块,初始化过程是异步的。理想情况下,我们不会在初始化实例和检索实例时都具有相同的功能。这样的事情存在吗?

我认为不一定有办法创建解决此问题的模式,但也许我犯的错误是以一种创建不需要的问题的方式构建我的代码存在 - 只使用一次值初始化模块的问题,但使用一个函数来初始化实例并检索实例。

1 个答案:

答案 0 :(得分:6)

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

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

因此,常见的方法是只为模块使用构造函数。在Javascript中,您可以允许构造函数采用提供初始化信息的可选参数。负责设置模块的代码将使用所需的setup参数调用构造函数。不负责设置模块的模块的其他用户可能只是不调用构造函数,或者如果他们想要返回值,或者他们应该传递其他构造函数参数,他们可以通过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');

注意:对于开发人员的理智,如果需要进行适当的设置(在可以正确使用之前)来检查模块是否已经在调用需要按顺序设置的任何方法时设置模块,这将非常有用在正确设置模块之前,正确地通知开发人员他们已经调用了方法。否则,错误可能会发生在下游更远的地方,并且可能没有有用的错误消息。

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

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