DEV Community

Cover image for Unlocking JavaScript Design Patterns: Mastering Singleton for Ultimate Code Efficiency
Nicolas B.
Nicolas B.

Posted on • Edited on

Unlocking JavaScript Design Patterns: Mastering Singleton for Ultimate Code Efficiency

In the world of JavaScript, design patterns are your secret weapon to crafting efficient, organized, and maintainable code. Among these patterns, the Singleton pattern stands out as a versatile powerhouse, enabling you to ensure a single instance of a class throughout your application. Let's see how it works !


The Singleton Pattern Demystified

The Singleton pattern, at its core, guarantees that a class has only one instance and provides a global point of access to that instance. This means that no matter how many times you request an instance of the class, you will a*lways get the same one. Singleton is particularly **useful when you need to manage shared resources or control access to a single point*, such as a configuration manager, database connection, or, in our case, a logging system.


The Logger Example

Setting Up the Logger

Imagine you're building a JavaScript application that consists of multiple modules and files. Debugging and tracking issues across the application can become challenging without a unified logging system. This is where the Singleton pattern comes to the rescue.

Let's create a simple logger that will maintain a single log throughout the application. Here's our logger.js file:

class Logger {
  constructor() {
    this.logs = [];
  }

  log(message) {
    this.logs.push(message);
  }

  showLogs() {
    console.log(this.logs);
  }

  static getInstance() {
    if (!this.instance) {
      this.instance = new Logger();
    }
    return this.instance;
  }
}

export default Logger;
Enter fullscreen mode Exit fullscreen mode

In this implementation, we've defined a Logger class that allows logging messages and displaying them. The getInstance static method ensures that only one instance of the Logger class exists throughout your application.

--

Use the Singleton Logger

Now, let's see how we can utilize the Singleton logger in different parts of our application.

app.js (Usage in One File)

import Logger from './logger.js';

const loggerInstance1 = Logger.getInstance();
loggerInstance1.log('Log message 1');
loggerInstance1.log('Log message 2');

loggerInstance1.showLogs();
Enter fullscreen mode Exit fullscreen mode

In app.js, we import the Logger instance, create a loggerInstance1, and use it to log messages. The beauty of the Singleton pattern is that we are always working with the same instance, ensuring that all logs are stored in one centralized location.

anotherFile.js (Usage in Another File)

import Logger from './logger.js';

const loggerInstance2 = Logger.getInstance();
loggerInstance2.log('Log message from another file');

loggerInstance2.showLogs();
Enter fullscreen mode Exit fullscreen mode

In anotherFile.js, we again import the same Logger instance and use loggerInstance2. Despite being in a different file, loggerInstance2 is essentially the same instance as loggerInstance1. This means that all logs from both files are accumulated in the same log array, giving you a comprehensive overview of your application's behavior.


Benefits of the Singleton Logger

Implementing the Singleton pattern for a logger in your JavaScript application offers several benefits:

1 - Centralized Logging: All log messages are collected in one place, making it easier to trace application behavior and debug issues.

2 - Consistency: You can be confident that you're working with the same logger instance throughout your application, ensuring data integrity.

3 - Easy Integration: The Singleton logger seamlessly integrates into different parts of your code, enhancing overall code efficiency.


Conclusion

The Singleton pattern is a powerful tool in JavaScript, providing a straightforward way to manage shared resources and maintain a single point of control in your application. As demonstrated through our logger example, it ensures that you have one consistent logger instance, simplifying debugging and enhancing code efficiency.

By mastering the Singleton pattern and using it wisely, you can unlock the full potential of your JavaScript applications, making them more organized, maintainable, and efficient.

Top comments (14)

Collapse
 
devdufutur profile image
Rudy Nappée • Edited

Actually in JS, any ES module is a singleton ! No need for getInstance() or any other fancy OOP stuff 😅

Collapse
 
syeo66 profile image
Red Ochsenbein (he/him)

Was about to say the same thing.

Collapse
 
zeroevidence profile image
Dale Moore

You beat me to it!

Collapse
 
teamradhq profile image
teamradhq

To expand on what others are saying regarding ES modules being singletons: what this means is that every module you load in your program is only evaluated once.

So, while you can import a module multiple times, any code that's executed within its immediate scope only runs on the first import.

In your example, the way you would implement Logger as singleton is to just export an instance of the class:

// logger.mjs
console.log('Logger module loaded');

class Logger {
  constructor() {
    this.logs = [];
  }

  log(message) {
    this.logs.push(message);
  }

  showLogs() {
    console.log(this.logs);
  }
}

export default new Logger();
Enter fullscreen mode Exit fullscreen mode

This guarantees that will only ever be one instance of Logger per process.

Consider this application structure:

// index.js
import logger from './logger.mjs';
import './module-a.mjs';
import './module-b.mjs';

console.log('Index loaded');
logger.log('Index:1');
logger.log('Index:2');
logger.showLogs();
Enter fullscreen mode Exit fullscreen mode
// module-a.mjs
import logger from './logger.mjs';

console.log('Module A loaded');
logger.log('Module A:1');
logger.log('Module A:2');
Enter fullscreen mode Exit fullscreen mode
import logger from './logger.mjs';

console.log('Module B loaded');
logger.log('Module B:3');
logger.log('Module B:4');
Enter fullscreen mode Exit fullscreen mode

When we run index.mjs this is the output:

#$ node ./index.js
Logger module loaded
Module A loaded
Module B loaded
Index loaded
[
  'Module A:1',
  'Module A:2',
  'Module B:3',
  'Module B:4',
  'Index:1',
  'Index:2'
]
Enter fullscreen mode Exit fullscreen mode

As you can see, all logs are sent to the same instance and modules are only evaluated the first time they're imported.

Collapse
 
brdnicolas profile image
Nicolas B.

Thank you for your very good explanation!

Collapse
 
teamradhq profile image
teamradhq • Edited

You're very welcome. I felt like "JS module is a singleton" isn't really an answer, and this fact doesn't make the singleton pattern redundant.

Exploiting JS module singleton is fine if you have a simple, single use case that doesn't require configuration. But if you have different implementations or use case requirements, then defining a singleton would be required.

An example of this in an Electron app I'm working on is handling IPC events when there are multiple render processes that need to handle events. Events need to be handled sequentially for each render process so I need some kind of queue system.

It's possible to achieve this with a JS module singleton. But it requires complex logic to determine which render process to delegate handling.

if (isMainWindowEvent(event) {
   mainWindow.invoke(handler)
} else if (isPlayerWindowEvent(event)) {
  playerWindow.invoke(handler)
} else {
  editorWindow.invoke(handler)
}
Enter fullscreen mode Exit fullscreen mode

There's a lot of problems here and this control flow needs to exist on any method that interacts with render processes. It will get messy pretty quickly.

It's much simpler to just define a singleton that accepts the render process and reference this instead:

const mainIpcHandler = new IpcEventHandler(mainWindow);
const playerIpcHandler = new IpcEventHandler(playerWindow);
const editorIpcHandler = new IpcEventHandler(editorWindow);
Enter fullscreen mode Exit fullscreen mode

So yeah, the fact that JS modules are singletons doesn't eliminate the need for singleton patterns in modular JS programs :)

Collapse
 
grunk profile image
Olivier • Edited

In your exemple nothing prevent 2 instance of the logger class which is the heart of the singleton "pattern".
Moreover your variable holding the instance reference is public hence the risk of letting the anyone resetting it to null.

A more secure approch could be :

class Logger {
  static #instance = null
  constructor() {
    if (Logger.#instance) {
      throw new Error("Singleton is limited to one instance. Use getInstance instead")
    }
    this.logs = [];
  }

  log(message) {
    this.logs.push(message);
  }

  showLogs() {
    console.log(this.logs);
  }

  static getInstance() {
    if (!Logger.#instance) {
      Logger.#instance = new Logger();
    }
    return Logger.#instance;
  }
}
Enter fullscreen mode Exit fullscreen mode

Instance is private to the class , constructor will throw an error if an other instance exists

Collapse
 
brdnicolas profile image
Nicolas B.

Very interesting approach, thank's for your feedback :)

Collapse
 
dsaga profile image
Dusan Petkovic

If es modules are used, we wouldn't even need to worry about multiple instances being created, we could just:

export const logger = new Logger();

Collapse
 
marcus_hoang profile image
Khánh Hoàng (Marcus)

I agree with @grunk comment, and we should make the access modifier of constructor is private to prevent new class

class Logger {
  static #instance = null
  private constructor() {
    if (Logger.#instance) {
      throw new Error("Singleton is limited to one instance. Use getInstance instead")
    }
    this.logs = [];
  }

  log(message) {
    this.logs.push(message);
  }

  showLogs() {
    console.log(this.logs);
  }

  static getInstance() {
    if (!Logger.#instance) {
      Logger.#instance = new Logger();
    }
    return Logger.#instance;
  }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
grunk profile image
Olivier

Unfortunately you can't in JS 😉

Collapse
 
dsaga profile image
Dusan Petkovic

If there are multiple methods that we can execute as part of the logging mechanism then probably a class approach like in the example would be a good solution vs just es modules, that way its more explicit

Collapse
 
pimp_my_ruby profile image
Pimp My Ruby

Very interesting thanks!

Collapse
 
shinigami92 profile image
Shinigami

I appreciate all the ESM comments, but I have a usecase where I create named logger instances.
So I call const logger = loggerFactory('MyService'); and behind that I have a Map<string, Logger>