Reputation: 6669
I am trying to define a Singleton in Javascript to be able to consume from different files.
class DataService {
constructor(options) {
this.models = options.models ;
this.data = {};
this.refresh();
}
refresh() {
Promise.all([
this.models.DATA.model.findAll({
raw: true,
attributes: ['key', 'value']
})
]).then(([data]) => {
this.data = this.parseData(data);
});
}
parseData(data) {
data.map(x => {
this.data[x.key] = JSON.parse(x.value);
});
return this.data;
}
}
module.exports = (options) => { return new DataService(options) };
I want to be able to import the module like this
const data = require('dataService')(options);
console.log('data.example', data.example);
I am not sure if it is possible to do this, since I am using async methods, and the data is not ready when I print the log.
Upvotes: 1
Views: 4696
Reputation: 35481
The way in which you can leverage modules to achieve a singleton-like pattern across all modules is to export an instance directly.
The reason this works is that require
caches the exports after the first import and thus will return that instance on all subsequent imports.
Right now you're exporting a function which, although it will always be the same function, has capabilities to always instantiate a new instance of your class and thus is breaking the singleton-pattern constraint you want to achieve (single instance across modules)
Because you want to externally specify the singleton instantiation options, one way you can do this with minimal changes to your code is to have the exported function return an instance if it already exists, rather than instantiate a new one:
let instance; // all files will receive this instance
module.exports = (options) => {
if (!instance) {
// only the first call to require will use these options to create an instance
instance = new DataService(options);
}
return instance;
}
This means that all files that do require('dataService')(options)
will receive the same instance and whichever file imports the module first is who's instantiation options will apply.
Do note that all subsequent calls will still have to be of the form require('dataService')()
(notice the extra invocation) which seems like a code-smell and would make the code harder to understand.
To make the code more readable, we could add some verboseness:
let instance; // all files will receive this instance
module.exports = {
getInstance(options) {
if (!instance) {
// only the first call to getInstance will use these options to create an instance
instance = new DataService(options);
}
return instance;
}
}
Which would be used like:
const instance = require('dataService').getInstance(options);
const instance = require('dataService').getInstance();
const instance = require('dataService').getInstance();
Another step could be to make the code more resilient to abuse by telling the programmer at run-time if they are using the API wrongly:
if (!instance) {
instance = new DataService(options);
} else if (options) {
// throw error on all subsequent calls with an options arg `require('dataService')(options)`
throw Error('Instance is already instantiate with `options`')
}
return instance;
This won't make the code more readable but would make it a bit safer.
If we interpret your API to mean "anytime options are passed, we should instantiate a new singleton", then you can consider maintaining a collection of instances instead, retrievable by some id (or maybe even the memory reference of the options themselves):
let instances = new Map();
module.exports = (options) => {
if (!instances.has(options.id)) {
instances.set(options.id, new DataService(options));
}
return instances.get(options.id);
}
The fact that you have async code in your singleton shouldn't matter. Time is not a property of a singleton, the requirement is to only have a single instance.
That being said, you might want to consider actually returning the promises created in your methods so that you can properly chain them or await on them:
class DataService {
constructor(options) {
this.models = options.models ;
this.data = {};
this.refresh();
}
refresh() {
return Promise.all(/* ... */).then(/* ... */);
//^^^^^^ return the promise chain so we can react to it externally
}
// ...
}
(async () => {
await new DataService().refresh(); // now you can do this
})()
Upvotes: 3
Reputation: 9988
That's how you can implement the Singleton with ES6:
class Singl {
constructor(options) {
console.log('calling constructor');
}
static getInstance(options) {
if (!Singl.instance) {
Singl.instance = new Singl(options);
}
return Singl.instance;
}
}
// the constructor will be called only once
Singl.getInstance();
Singl.getInstance();
Singl.getInstance();
As you can see from the snippet, the constructor will be called just the first time you call getInstance
.
Then you should be able to export the getInstance
method and pass options:
module.exports = Singl.getInstance;
Upvotes: 2