
Data Structure
Networking
RDBMS
Operating System
Java
MS Excel
iOS
HTML
CSS
Android
Python
C Programming
C++
C#
MongoDB
MySQL
Javascript
PHP
- Selected Reading
- UPSC IAS Exams Notes
- Developer's Best Practices
- Questions and Answers
- Effective Resume Writing
- HR Interview Questions
- Computer Glossary
- Who is Who
Advanced JavaScript Patterns: Singleton, Decorator, and Proxy
JavaScript is a versatile programming language that allows developers to create dynamic and interactive web applications. With its vast array of features and flexibility, JavaScript enables the implementation of various design patterns to solve complex problems efficiently. In this article, we will explore three advanced JavaScript patterns: Singleton, Decorator, and Proxy. These patterns provide elegant solutions for managing object creation, adding functionality dynamically, and controlling access to objects.
Singleton Pattern
The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. This pattern is useful when we want to limit the number of instances of a class to prevent redundancy or ensure that a single instance is shared across the application.
Example
Let's consider an example where we want to create a logger object that maintains a single instance throughout the application:
// Singleton Logger const Logger = (() => { let instance; function createInstance() { const log = []; return { addLog: (message) => { log.push(message); }, printLogs: () => { log.forEach((message) => { console.log(message); }); }, }; } return { getInstance: () => { if (!instance) { instance = createInstance(); } return instance; }, }; })(); // Usage const logger1 = Logger.getInstance(); const logger2 = Logger.getInstance(); logger1.addLog("Message 1"); logger2.addLog("Message 2"); logger1.printLogs();
Explanation
In this example, we use an immediately-invoked function expression (IIFE) to encapsulate the Singleton implementation. The getInstance method checks if an instance of the logger exists. If it does not, it creates a new instance using the createInstance function. This ensures that only one instance of the logger is created and shared among all the calls to getInstance. The logger instance maintains an internal log array, allowing messages to be added and printed.
Decorator Pattern
The Decorator pattern allows us to add new behaviors or modify existing ones dynamically to an object without affecting other instances of the same class. It enables us to extend the functionality of an object by wrapping it with one or more decorators, providing a flexible alternative to subclassing.
Example
Let's consider an example where we have a simple car object and want to add additional features using decorators:
// Car class class Car { constructor() { this.description = "Basic car"; } getDescription() { return this.description; } } // Decorator class class CarDecorator { constructor(car) { this.car = car; } getDescription() { return this.car.getDescription(); } } // Decorators class ElectricCarDecorator extends CarDecorator { constructor(car) { super(car); } getDescription() { return `${super.getDescription()}, electric engine`; } } class LuxuryCarDecorator extends CarDecorator { constructor(car) { super(car); } getDescription() { return `${super.getDescription()}, leather seats`; } } // Usage const basicCar = new Car(); const electricLuxuryCar = new ElectricCarDecorator( new LuxuryCarDecorator(basicCar) ); console.log(electricLuxuryCar.getDescription());
Explanation
In this example, we start with a Car class representing a basic car with a getDescription method. The CarDecorator class serves as the base decorator, providing a common interface for all decorators. Each specific decorator extends the CarDecorator and overrides the getDescription method to add or modify functionality.
We create two specific decorators: ElectricCarDecorator and LuxuryCarDecorator. The ElectricCarDecorator adds an electric engine feature, while the LuxuryCarDecorator adds leather seats. By combining these decorators, we can enhance the car object with multiple features dynamically.
Proxy Pattern
The Proxy pattern allows us to control access to an object by providing a surrogate or placeholder. It can be used to add additional logic, such as validation or caching, without modifying the original object's behaviour. Proxies act as intermediaries between clients and the actual objects, enabling fine-grained control over object interactions.
Example
Let's consider an example where we use a proxy to implement simple access control for a user object:
// User object const user = { name: "John", role: "user", }; // Proxy const userProxy = new Proxy(user, { get: (target, property) => { if (property === "role" && target.role === "admin") { throw new Error("Access denied"); } return target[property]; }, }); console.log(userProxy.name); console.log(userProxy.role);
Explanation
In this example, we have a user object with properties name and role. We create a proxy using the Proxy constructor, providing a handler object with a get trap. The get trap intercepts property access and allows us to implement custom logic. In this case, we check if the property being accessed is role and if the user has the "admin" role. If the condition is met, we throw an error to deny access; otherwise, we allow the property access.
Conclusion
Understanding advanced JavaScript patterns such as Singleton, Decorator, and Proxy opens up a new world of possibilities for developers. These patterns provide elegant solutions to manage object creation, dynamically add functionality, and control access to objects. By leveraging these patterns, developers can write cleaner, more maintainable code that is flexible and extensible.
By implementing the Singleton pattern, we ensure that only one instance of a class exists throughout the application, preventing redundancy and allowing shared resources. The Decorator pattern allows us to dynamically add or modify the behaviour of an object without affecting other instances. Finally, the Proxy pattern gives us fine-grained control over object access, allowing us to enforce rules and implement additional logic.
By incorporating these advanced JavaScript patterns into our codebase, we can build more robust and scalable applications, making our code more flexible and maintainable in the long run.