Common Node js design patterns
Node.js design patterns are one of the most frequently asked questions in senior-level interviews. In this blog, we will discuss some common Node.js design patterns that are frequently use while working with Node.js. Before moving forward, let's answer what a design pattern is.
What are design patterns?
A design pattern is a general, reusable solution to a common problem that occurs in software design. It is a template or blueprint that can be applied in various programming contexts to solve recurring design issues in a systematic way. Design patterns aren't specific to a particular programming language but represent best practices that are adaptable across different languages.
Node.js offers several design patterns that help developers structure their applications in a scalable, maintainable, and efficient way. In this blog we will explore common node js paterns.
1. Singleton Pattern
Sometimes, we need to make sure that only one instance of the class has been created. Singleton Pattern Ensures that a class has only one instance and provides a global point of access to it.
Consider a database connection. We don’t need more than one database connection for the app at a given time. We Can reuse existing connection through out our app. In these type of scenerio singleton pattern is very useful.
In Node.js, we implement singleton pattern using IIFE modules or by creating ES6 class. Below is the example of a Datbase
singleton class.
Below, we are creating a database connection only if the database class has not been instantiated. If the database class has been instantiated,
we simply return the instance of the database. This is why instance1 === instance2
returns true because it references the same object..
Benefits:
- Global Access: A convenient way to access shared data or functionality from anywhere in your application.
- Resource Management: Ensures efficient use of resources like database connections, loggers, or file handles by having only one instance.
- Consistency: Enforces consistent behavior as modifications affect only a single instance.
- Controlled State: Simplifies state management by having a central point for data manipulation.
2. Factory Pattern
Factory pattern creates objects without exposing the instantiation logic to the client and refers to the newly created objects through a common interface.
The Factory pattern provides an interface for creating objects but lets subclasses alter the type of objects created. This help us in placing the object creation logic at one place.
Consider it as a manufacturing plant with different assembly lines for producing distinct products. In Node.js, the Factory pattern excels in creating objects without specifying their concrete classes, fostering flexibility and extensibility.
Below, I have provided an example of an animal factory pattern. The AnimalFactory contains the logic for creating the type passed to it.
It creates a Dog
object if the type is 'dog' and a Cat
object if the type is 'cat'.
Benefits:
- Decoupling: Client code is decoupled from specific object creation logic, promoting flexibility and maintainability.
- Centralized Control: You can easily add new object types or modify existing ones without affecting client code, as long as the Factory handles the changes.
- Flexibility: The Factory can choose the appropriate object based on runtime conditions or configuration, making your code more adaptable.
- Encapsulation: Object creation details are hidden within the Factory, improving code readability and maintainability.
3. Observer Pattern
The next design pattern, the Observer Pattern, is one of the most widely used design patterns in event-driven architecture. This is used to notify multiple objects about any state changes in an object they are observing. The Observer Pattern is a behavioral design pattern that defines a one-to-many relationship between objects. In this pattern, when one object (the "subject") changes its state, all dependent objects (the "observers") are automatically notified and updated.
The common structure of an Observer pattern is shown in the example code below. The Subject class manages a list of observers
and provides methods to add, remove, and notify them. When an observer is added using the addObserver()
method, it becomes part
of the subject's list of observers. If an observer is no longer needed, it can be removed using the removeObserver()
method.
When the subject wants to notify all observers about a change, it calls the notifyObservers()
method, which sends a message to
each observer by invoking their update()
method.
The Observer class represents objects that want to receive updates from the subject. Each observer is created with a name, and its
update()
method prints a message to the console indicating that it received a notification. In the example, two observers, "Observer 1"
and "Observer 2," are created and registered with the subject. When the subject calls notifyObservers()
with the message "Subject state
has changed!", both observers receive the message, and their update() methods log the received message along with their names.
In nodejs, this pattern often used for real-time communication, event handling, or messaging systems. Below is the example of observer patern using nodejs event class.
Benefits:
- Loose Coupling: The subject doesn't need to know details about its observers. This decouples the subject and observers, making the system more flexible.
- Scalability: You can add any number of observers, and they will all be updated when the subject changes.
- Modularity: The logic of handling changes is separated from the main business logic of the subject, improving the maintainability of code.
4. Proxy Pattern
The Proxy Pattern provides an object (the proxy) as an interface to another object (the real subject). The proxy controls access to the real object, allowing additional functionality such as lazy initialization, access control, logging, or caching before passing the request to the real object. In below example, we have an expensive object that we want to initialize only when it's needed (lazy initialization).
In above example proxy (ExpensiveObjectProxy) holds a reference to the real subject (ExpensiveObject), but it doesn't instantiate the object immediately. The proxy delays the creation of the real object until getData() is called for the first time. This is called lazy initialization. On subsequent calls, the proxy checks if the real object has already been created. If it has, it forwards the request directly to the real object without reinitializing it.
In real-world applications, the Proxy Pattern can be used for API rate limiting. The proxy can act as an intermediary to limit the number of API calls made by a client within a certain time frame.
The Proxy Pattern is useful when:
- Lazy Initialization: The real object is resource-intensive to create, so the proxy delays the creation until it's needed.
- Access Control: The proxy can restrict or validate access to the real object.
- Logging: The proxy can log requests before forwarding them to the real object.
- Remote Proxy: The proxy can represent a remote object that resides in a different address space (such as another server or a database).
5. Decorator Pattern
This pattern allows behavior to be added to individual objects, without affecting the behavior of other objects from the same class. The main idea behind the Decorator Pattern is to wrap an object with a "decorator" object that adds new behaviors or responsibilities, while still exposing the same interface as the original object.
Key Concepts:
- Component: The base interface or class that defines the common behaviors that can be extended.
- Concrete Component: The original object that implements the base interface and will be decorated.
- Decorator: A class that wraps the concrete component, extending its behavior without modifying its structure.
- Concrete Decorators: Specific classes that add behavior to the component by wrapping it.
Let's use a coffee order system to explain the Decorator Pattern. The customer orders a base coffee, and then they can add various condiments like milk, sugar, or whipped cream.
Output:
In the above example the basic coffee object with a cost() and description() method. Concrete Decorators (MilkDecorator, SugarDecorator, WhippedCreamDecorator) takes an instance of Coffee (or a previously decorated coffee) and wraps it, adding its own functionality (e.g., increasing cost, updating description). The client dynamically combines decorators to customize the object behavior.
Advantages of the Decorator Pattern:
- Flexibility: Behaviors can be added or removed at runtime, allowing for more flexibility than subclassing.
- Avoids Class Explosion: Instead of creating multiple subclasses for each combination of behavior, you can create simpler decorators and mix them as needed.
- Single Responsibility Principle: Each decorator handles a single, specific enhancement, leading to more maintainable and understandable code. Drawbacks:
- Complexity: The pattern can add complexity to the code, as you are adding multiple objects (decorators) that wrap around the core object.
- Order Matters: Since decorators are applied dynamically, the order in which they are applied can affect the result, which may lead to unintended behaviors if not handled carefully.
6. Module Pattern
Modular pattern helps in organizing code by grouping related functionality, and it provides a way to mimic the concept of classes and private/public access in a language like JavaScript, which doesn't natively support private members. The goal of the Module Pattern is to maintain clean, modular code by restricting access to parts of the code and exposing only what is necessary. It uses javascript closures to create a private scope.
A basic structure of Module Pattern shown below:
In the above example, Variables (privateVar) and methods (privateMethod) are declared within the function's local scope, so they aren't accessible outside the function. The publicMethod is exposed via the return object, allowing external access to interact with the module. This method can access private members inside the module.
A real life example: Shopping Cart Module
In the above example the cart array and the methods addItem, getTotalItems, and displayCart are private and cannot be accessed directly. Methods like add, show, and totalItems are exposed through the returned object, which is the public API of the ShoppingCart module.
IIFE (Immediately Invoked Function Expression): The Module Pattern commonly uses an IIFE to create a closure that helps in maintaining privacy. The function is invoked immediately after it's defined, and the returned object becomes the module's public interface.
ES6 Modules: In modern JavaScript, ES6 introduced native module support. Using ES6 modules provides many of the same benefits as the Module Pattern, including encapsulation and reusability, but without needing to rely on IIFEs.
Here's an example using ES6 syntax cart.js (module file):
main.js
Node.js uses the CommonJS module system, allowing you to split your code into separate modules for better maintainability and reusability.
Advantages of the Module Pattern:
- Encapsulation: It promotes data hiding and ensures that internal implementation details aren't exposed.
- Maintainability: By splitting the code into logical modules, it's easier to manage and maintain.
- Avoids Global Scope Pollution: The Module Pattern keeps variables and methods out of the global namespace, reducing the risk of conflicts.
- Public/Private Separation: It clearly separates the public API from the private implementation details. Disadvantages:
- Overhead: The IIFE and closures may introduce some runtime overhead compared to more straightforward approaches like ES6 modules.
- Testability: Private methods are harder to test since they aren't exposed to the outside.
7. Callback Pattern
Node.js is asynchronous, relying heavily on callbacks to handle asynchronous operations.
Problems:
- Can lead to "callback hell" when nested callbacks become too complex.
8. Promise Pattern
Promises provide a cleaner and more readable way to handle asynchronous operations compared to callbacks.
Benefits:
- Avoids callback hell
- Easier error handling with .catch()
- Can be further simplified with async/await
9. Async/Await Pattern
A more modern approach for handling asynchronous code, built on top of Promises.
Benefits:
- Synchronous-looking asynchronous code
- Easier to read and maintain
10. Middleware Pattern
Commonly used in frameworks like Express.js, middleware functions are used to handle requests, responses, and route handling in a modular fashion.
Benefits:
- Extensibility of functionality
- Modular code structure
Conclusion
Node.js design patterns help in building more maintainable and scalable applications. By using these patterns effectively, you can optimize your code for better performance, organization, and reusability.