Singleton Pattern

designpatterns
Published

January 1, 2024

The Singleton pattern is one of the most well-known creational design patterns. Its core purpose is to ensure that a class has only one instance and provides a global point of access to it. This is particularly useful when you need to control access to a shared resource, manage a single configuration object, or implement a logger. Let’s look at how to implement this pattern effectively in JavaScript.

The Problem: Multiple Instances

Imagine you’re building a system that needs a single database connection. Without the Singleton pattern, you could accidentally create multiple connections, leading to resource conflicts and inefficient use of database resources. The Singleton pattern prevents this.

Implementing the Singleton Pattern

There are many ways to implement the Singleton pattern in JavaScript. We’ll look at two common approaches:

1. Using a Static Property:

This approach uses a static property to hold the single instance and a static method to retrieve it.

class Database {
  static instance = null;

  constructor(dbName) {
    if (Database.instance) {
      return Database.instance;
    }
    this.dbName = dbName;
    Database.instance = this;
  }

  connect() {
    console.log(`Connecting to database: ${this.dbName}`);
    // Database connection logic here...
  }
}

const db1 = new Database('myDB');
const db2 = new Database('anotherDB'); // This will return the same instance as db1

db1.connect(); // Connects to myDB
db2.connect(); // Connects to myDB (same instance)
console.log(db1 === db2); // true

This ensures that only one instance of the Database class is ever created. Subsequent calls to the constructor return the existing instance.

2. Using a Function-Based Approach (IIFE):

Another popular method uses an Immediately Invoked Function Expression (IIFE) to encapsulate the instance creation.

const Database = (function() {
  let instance;

  function createInstance(dbName) {
    const db = {
      dbName,
      connect() {
        console.log(`Connecting to database: ${this.dbName}`);
        // Database connection logic here...
      }
    };
    return db;
  }

  return {
    getInstance: function(dbName) {
      if (!instance) {
        instance = createInstance(dbName);
      }
      return instance;
    }
  };
})();


const db1 = Database.getInstance('myDB');
const db2 = Database.getInstance('anotherDB'); // This will return the same instance as db1

db1.connect(); // Connects to myDB
db2.connect(); // Connects to myDB (same instance)
console.log(db1 === db2); // true

This method offers a slightly more concise way to achieve the same result. The IIFE ensures that the instance variable is privately scoped.

When to Use the Singleton Pattern

The Singleton pattern is powerful, but it’s not always the best solution. Overuse can lead to tightly coupled code and make testing more difficult. Consider using it when:

  • You need a single point of access to a shared resource.
  • You need to control the creation of instances.
  • You want to ensure only one instance exists.

Remember to carefully evaluate the trade-offs before implementing the Singleton pattern in your projects. Consider alternatives like dependency injection if the benefits don’t outweigh the potential downsides.

Extending the Singleton Pattern for More Control

You can expand upon these basic implementations by adding error handling, configuration options, or other features as needed for your specific use case. For example, you might want to add logging or validation to the connect() method. The core principle of maintaining a single instance remains consistent regardless of complexity.

Exploring Alternatives

While the Singleton pattern solves many problems, understanding its limitations is crucial. Alternatives such as dependency injection offer greater flexibility and testability in many scenarios. Weighing the pros and cons of different approaches is vital for a well-architected application.