Search code examples
node.jssingleton

What is the difference between a singleton and a module that instantiates an instance?


I want to add a metrics class to my code. What is the difference between

class MyMetrics {
  constructor () {
    if (!Singleton.instance) {
      Singleton.instance = new Metrics()
    }
    return Singleton.instance
  }
}

const metrics = new MyMetrics()

and

export const metrics = new Metrics()

Wouldn't each module that imported metrics be using the same Metrics instance?

Are they functionally the same for my usage? Which would be recommended?


Solution

  • Wouldn't each module that imported metrics be using the same Metrics instance?

    Yes, they would.

    Are they functionally the same for my usage?

    As long as A) you aren't creating other instances within the module and B) you aren't exporting Metrics, yes, almost. But one thing to remember is that any code that has access to your metrics import has access to your Metrics constructor, indirectly via the constructor property metrics inherits from Metrics.prototype:

    import { metrics } from "./your-module.js";
    const newMetrics = new metrics.constructor();
    

    You might think that with the singleton, you've avoided that, but that's easily defeated as instance is public:

    import { metrics } from "./your-module.js";
    const Metrics = metrics.constructor;
    Metrics.instance = null;
    const newMetrics = new Metrics();
    

    So you might want to make it private (either using a static private property, or just using metrics itself to check if you've created it).

    Which would be recommended?

    That's a matter of opinion. But you might not even need a class at all. You could:

    • Just make an object directly without a constructor function.
    • Export functions and close over module-private variables.

    For instance, consider this (fairly silly) class:

    // stuff.js
    class Stuff {
        #items = new Map();
        constructor() {
            // ...pretend there's singleton logic here...
        }
        put(key, value) {
            this.#items.set(key, value);
        }
        get(key) {
            return this.#items.get(key);
        }
    }
    export const stuff = new Stuff();
    

    The way you use that is:

    import { stuff } from "./stuff.js";
    
    stuff.put("this", "that");
    stuff.get("this"); // "that"
    

    You could just get rid of the class entirely:

    // stuff.js
    const items = new Map();
    export const stuff = {
        put(key, value) {
            items.set(key, value);
        },
        get(key) {
            return items.get(key);
        }
    };
    

    Usage would be the same:

    import { stuff } from "./stuff.js";
    
    stuff.put("this", "that");
    stuff.get("this"); // "that"
    

    Or you could just export put and get:

    // stuff.js
    const items = new Map();
    export const put = (key, value) => {
        items.set(key, value);
    };
    export const get = (key) => items.get(key);
    

    Then usage is:

    import { get, put } from "./stuff.js";
    
    put("this", "that");
    get("this"); // "that"
    

    Or if those names may conflict with other things:

    import { get as getStuff, put as putStuff } from "./stuff.js";
    
    putStuff("this", "that");
    getStuff("this"); // "that"
    

    So you have lots of options to choose from. Constructor functions (including ones created with class syntax) are useful if you need to construct multiple objects with shared characteristics, but if you aren't doing that (and you aren't with a singleton), just writing the object directly or (with modules) exporting the things it can do directly may be good options for you.