简体   繁体   中英

Node.js singleton module pattern that requires external inputs

normally we might create a simple singleton object like so with Node.js:

var foo = {};

module.exports = foo;

or

function Foo(){}

module.exports = new Foo();

however

what is the best way to make a clean singleton module that needs an external variable for initialization? I end up making something like this:

var value = null;

function init(val){

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

  return value;
}

module.exports = init;

this way someone using the module can pass in an initializing value for a certain variable. Another way to do it would be like so:

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

var instance = null;

module.exports = function init(value){

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

}

there's two problems I encounter with this:

(1) this is minor, but the semantics is wrong. We can rename init to getInstance, but we can't make the same function literal mean "initialize and get" since they are different meanings. So we have to have a function that does two different things. Create an instance and retrieve and instance. I don't like this especially since in some cases we need to make sure the argument to initialize the instance is not null. With multiple developers using a module it's not clear if a module has been initialized yet, and if they pass in undefined into the module that hasn't been initialized, that could become a problem or just confusing at the least.

(2) this is more important - in some cases initializing Baz is asynchronous. For example, making a Redis connection or reading from a file to initialize a singleton, or making a socket.io connection. This is what really trips me up.

eg here is a module that I have that I consider really ugly that stores a socket.io connection:

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

the above module is initialized like so:

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

So I am looking for a design pattern that allows us to create a singleton module where the initialization process is asynchronous. Ideally we won't have the same function both initializing the instance as well as retrieving it. Does such a thing exist?

I don't think there is necessarily a way to create a pattern that solves this problem but perhaps I am making the mistake of structuring my code in a way that is creating a problem that doesn't need to exist- the problem of initializing a module with a value only once, but using one function to both init the instance and retrieve the instance.

It sounds like you're trying to create a module that gets initialized in one place and then uses some shared resource from that initialization for other users of that module. That is a semi-common need in the real world.

First off, it's ideal if a module can load or create the things that it depends on because that makes it more modular and useful on its own and puts less of a burden on someone using it. So, in your case, if your module could just create/load the thing that it needs when the module is first created and just store that resource in it's own module variable, then that would be the ideal case. But, that is not always possible because the shared resource may be someone else's responsibility to set up and initialize and this module just needs to be made aware of that.

So, the common way to do that is to just use a constructor function for the module. In Javascript, you can allow the constructor to take an optional argument that provides the initialization info. The code responsible for setting up the module would call the constructor with the desired setup parameter. Other users of the module that weren't responsible for setting up the module could just either not call the constructor or if they want a return value or there are other constructor parameters that they should pass, they could pass null for that setup parameter.

For example, you could do this:

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

Then, users of the module could either do this:

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

or this:

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

Note: For developer sanity, it would be useful to have individual methods that require appropriate setup (before they can be used properly) to check to see if the module has been setup when any method is called that needs that setup in order to properly inform a developer that they have called a method before setting up the module properly. Otherwise, errors can happen much further downstream and likely won't have useful error messages.


If you now want the initialization process to be async, that can be done too, but it certainly complicates other uses of the module because they won't necessarily know when/if the module has been initialized.

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

If you have other users of this module that wish to be notified when it has finished initializing, you can allow them to register a callback themselves to be notified when the module is ready.

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

If, for reasons I don't quite understand, you want to use the same constructor for both initialization and ready detection, that can be done, though I don't think it's near as clear as just using a separate method for ready detection:

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

Usage for async initializing the module:

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

Usage for loading the module and getting notified when someone else has initialized it:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM