DEV Community

Cover image for Implement the Singleton pattern
Phuoc Nguyen
Phuoc Nguyen

Posted on • Originally published at phuoc.ng

Implement the Singleton pattern

The Singleton pattern is just one of many design patterns in software development. Design patterns are like reusable templates that solve common problems in software design. They help us create code that's easy to maintain, scale, and make robust.

There are three main types of design patterns: creational, structural, and behavioral. Creational patterns help us create objects in specific situations. Structural patterns focus on how classes and objects are put together to form larger structures. Behavioral patterns are all about how objects interact with each other and share responsibilities.

The Singleton pattern is a creational pattern because it helps us create objects. It ensures that only one instance of a class is created and that the instance is globally accessible throughout the application.

In this post, we'll dive deeper into the Singleton pattern and learn how to use JavaScript Proxy to implement it.

Demystifying the Singleton design pattern

To better understand the Singleton pattern, let's take an example. Suppose we have a class called DatabaseConnection that connects to a database and exposes some methods to execute queries.

We want to ensure that only one instance of this class is created throughout the application because creating multiple instances would be expensive and unnecessary. That's where the Singleton pattern comes in handy.

Here's how we can implement the DatabaseConnection class using the Singleton pattern:

class Database {
    query(sql) {
        // ...
    }
}

const DatabaseConnection = (() => {
    let instance;

    return {
        getInstance: () => {
            if (!instance) {
                instance = new Database();
            }
            return instance;
        },
    };
})();
Enter fullscreen mode Exit fullscreen mode

In this example, we're using an IIFE (Immediately Invoked Function Expression) to create a closure that encapsulates the DatabaseConnection class. We define a private variable called instance that holds the single instance of the class.

The reason we use an IIFE to create the DatabaseConnection class is to keep it private and prevent any potential naming collisions or interference with other parts of our application.

The public method getInstance() checks if an instance of the class already exists. If it does, it returns that instance. Otherwise, it creates a new instance by calling new Database() and sets it as the value of instance.

To use the Singleton, simply call its public method getInstance().

const connection1 = DatabaseConnection.getInstance();
const connection2 = DatabaseConnection.getInstance();

console.log(connection1 === connection2);   // true
Enter fullscreen mode Exit fullscreen mode

The beauty of the Singleton is that it ensures there is only one instance of the class in our application. We can see from our usage example above that both variables hold the same instance of the class because they are strictly equal (===).

Using Proxy to implement the Singleton pattern

Another way to implement the Singleton pattern in JavaScript is by using Proxy to modify the class constructor. With this approach, we can trap the constructor of the DatabaseConnection class using a Proxy object.

Here's how we can modify our previous implementation to use Proxy:

class Database {
    // ...
}

const DatabaseConnection = (() => {
    let instance;

    // Create a proxy for the class constructor
    const handler = {
        construct(target, args) {
            if (!instance) {
                instance = new Database();
            }
            return instance;
        },
    };

    return new Proxy(function() {}, handler);
})();
Enter fullscreen mode Exit fullscreen mode

In this example, we're creating a new Proxy object that uses a special method called construct to intercept any attempts to create a new instance of our class using the new keyword.

Inside the construct method, we first check if an instance of the class already exists. If it does, we simply return that instance. Otherwise, we create a new instance of the class by calling new Database() and set it as the value of instance.

Finally, we replace our original variable declaration with a call to the empty constructor function wrapped in our proxy object. This ensures that all attempts to create a new object of type DatabaseConnection are intercepted by our proxy handler.

By using this approach, we can guarantee that our singleton class will only ever have one instance. Any attempts to create additional instances will simply return a reference to the existing instance.

const connection1 = new DatabaseConnection();
const connection2 = new DatabaseConnection();

console.log(connection1 === connection2);   // true
Enter fullscreen mode Exit fullscreen mode

Creating Singleton instances for any class

We know how to implement the Singleton pattern for a specific class, but what if we want to create singletons for multiple classes? It's not practical to write similar code for each class. Luckily, we can create a generic function that returns a singleton instance of any class.

Here's how we can achieve this:

const createSingleton = (Class) => {
    let instance;

    return new Proxy(Class, {
        construct(target, args) {
            if (!instance) {
                instance = new target(...args);
            }
            return instance;
        },
    });
};
Enter fullscreen mode Exit fullscreen mode

In this example, we have a function called createSingleton() that takes a class as an argument and returns a singleton instance of that class.

The function creates a private variable called instance and uses Proxy to trap the construct method. Inside the construct method, we check if an instance of the class already exists. If it does not exist, we create a new instance by calling new target(...args) (where target refers to the input argument Class) and set it as the value of instance.

Finally, we return our proxy object with the trapped constructor. With this function, we can create singletons for any class.

For example, let's say we have a class called Logger. We can use createSingleton(Logger) to create a singleton instance of Logger.

class Logger {
    log(message) {
        console.log(`[Logger] ${message}`);
    }
}

const SingletonLogger = createSingleton(Logger);
Enter fullscreen mode Exit fullscreen mode

Now, we can create two objects from this singleton and confirm that both variables are referencing the same object by checking their strict equality using ===.

const logger1 = new SingletonLogger();
const logger2 = new SingletonLogger();

console.log(logger1 === logger2);   // true
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this post, we talked about design patterns, specifically the Singleton pattern. We learned how to use JavaScript Proxy to implement the Singleton pattern and modify the class constructor to create a single instance of any class.

Using the Singleton pattern can help optimize performance and memory usage by ensuring that only one instance of a class is created throughout the application. This is especially helpful for classes that are expensive to create or have shared state.

However, it's important to use the Singleton pattern wisely. Overusing singletons can lead to tight coupling between modules, making code harder to test and maintain. Before deciding to use this pattern, it's important to weigh the benefits against the potential drawbacks.


If you found this series helpful, please consider giving the repository a star on GitHub or sharing the post on your favorite social networks 😍. Your support would mean a lot to me!

If you want more helpful content like this, feel free to follow me:

Top comments (0)