Concept
Design patterns can be described as thoroughly established solutions to frequently encountered challenges in software development. It can be quite exhausting for programmers to struggle with issues that have already been addressed by others. Every programmer aims to create code that meets industry standards, ensuring it is adaptable to various situations, maintainable, easily readable, and reusable. To achieve this, it is essential to implement a proper code structuring method or pattern, making it vital to tackle this challenge effectively. This is precisely where design patterns become significant, as they offer a foundation for addressing these prevalent issues that emerge in specific contexts.
Design patterns are essential because of their stability over time; they can be consistently implemented across applications, whether they are standard or complex in nature.
In this tutorial, we will explore various design patterns in JavaScript. Many JavaScript developers often engage with design patterns without even realizing it while they build applications. This guide will provide detailed explanations and examples of the key and commonly utilized JavaScript Design Patterns.
Benefits of Design Patterns
They represent the optimal solutions: Design patterns are commonly utilized by developers across the board, as they understand the effectiveness of these patterns. Additionally, developers are generally familiar with design patterns since they have undergone numerous revisions prior to being put into practice.
They are adaptable: Design patterns represent a solution that can be reused and adjusted in various scenarios to address a range of issues, as they are not confined to resolving just one specific problem.
They possess clarity: Design patterns require no additional clarification, as they can effectively break down complex issues into manageable components.
They improve communication: When developers are knowledgeable about design patterns, they can establish shared objectives for a given issue, facilitating discussions regarding potential risks and solutions related to those challenges.
No code refactoring: Design patterns are frequently described as ideal solutions for a range of challenges. When an application is developed with design patterns as a guiding principle, it is presumed that the resulting solution is the sole effective approach and therefore does not require any code refactoring.
They reduce the size of the codebase: As the sole effective solution, design patterns help conserve valuable space by executing a necessary solution in just a few lines of code while maintaining a minimal footprint.
Having acquired foundational knowledge regarding design patterns in JavaScript, let's delve into their application by examining coding examples.
Categories
JavaScript Design Patterns encompass a range of classifications. The accompanying image provides a comprehensive overview of all the available patterns.
In this section, we will delve into an in-depth discussion of these three design patterns, accompanied by appropriate examples.
Creational Design Patterns
The design patterns mentioned previously focus primarily on the instantiation of classes. This concept can be expanded by differentiating between class creation and object creation patterns. These design patterns address the development of frameworks that simplify the complexities associated with class-object relationships into more straightforward and manageable patterns. More intricate design patterns often require extensive strategies and time investment; however, this challenge can be mitigated through creational design patterns. These patterns manage class inheritance efficiently and leverage object delegation effectively to accomplish tasks. Several well-known design patterns have already been illustrated in the image above. Now, let us explore some of the categories in greater detail.
Constructor Pattern
The construct pattern stands out as straightforward, contemporary, and ranks among the most widely utilized subcategories of creational design patterns within JavaScript. Its primary objective is to streamline the process of creating constructors. Take a look at the following example:
class Automobile {
constructor(brand, mileage) {
this.brand = brand;
this.mileage = mileage;
this.getDetails = function () {
console.log(`${this.brand} gives ${this.mileage}kmpl!`);
};
}
}
const Automobile1 = new Automobile('Mercedes',20.8);
Automobile1.getDetails();
Output
Mercedes gives 20.8 kmpl!
In this example, we have created a class/function named Automobile that includes attributes such as brand and mileage. The getDetails method demonstrated in this example will display the specified brand and mileage of the automobile in the format illustrated in the output above. We have instantiated an object for the class/function by invoking the constructor method with the new keyword for the specified attributes.
Prototype Pattern
Prototyping essentially refers to the process of duplication. By utilizing prototype patterns, we can create a new object based on the structure of an already existing object through cloning techniques. Subsequently, this is enhanced by prototypal inheritance, which leverages the inherent prototypical capabilities of JavaScript. A clearer understanding of this concept can be gained by examining the accompanying image and the code example provided below.
const cheetah = {
Speed: 120,
start() {
return 'Running';
},
stop() {
return 'Resting';
},
};
const cheetah1 = Object.create(cheetah, { Trainer: { value: 'John' } });
console.log(cheetah1.__proto__ === cheetah); // true
In this illustration, we have established a prototype for a cheetah, which is subsequently duplicated to generate a new object utilizing the Object.create method. This is achieved by defining a constant named cheetah1, where the Trainer's name will be saved following the conventional ES6 approach.
Structural Design Patterns
This design pattern focuses on the composition of classes and objects and the relationships between them. It establishes structurally defined approaches for classes and objects to guarantee that modifications in one part of a system do not necessitate changes in the entire system. This is achieved by identifying class-creation patterns that leverage inheritance for composing interfaces and utilizing object-creation patterns to outline methods for introducing new functionalities. Additionally, it encompasses several categories that have been illustrated in the image above. Let’s explore some of these categories further.
Adapter Design Pattern
The adapter design pattern facilitates the collaboration of various object designs through a unified interface. Imagine we are developing a library that processes data structures and formats specifically in JSON. In this scenario, we have an existing legacy API that provides responses in XML format. These XML responses are subsequently utilized to create charts, which exclusively accept JSON objects. Consequently, we implement an adapter function to transform this XML into JSON whenever necessary. This adapter function serves as a bridge, enabling the integration of two distinct object types. Refer to the image below for illustration.
Composite Design Pattern
The composite design pattern is among the most frequently utilized structural design patterns, enabling the composition of objects into tree-like structures. These structured trees can subsequently be managed as single entities. To illustrate this, let us examine a jQuery-based example that demonstrates the organization of the composite design pattern.
$("#element").addClass("blur") // element is an id
$(".element").addClass("blur") // element is a class
$("div").addClass("blur") // native element div
$(".element.isActive").addClass("blur") // trying to access a DOM node
that has element as well as isActive class
$(".element h2").addClass("blur") // trying to access the native
element h2 which is inside a node with class element
In the preceding example, jQuery simplifies the process of executing various methods on the chosen DOM elements by providing straightforward access to combinations. The addClass method is utilized to conceal the complexities of the implementation. Its primary function is to ensure that a collection of objects operates as if they were separate entities.
Behavioral Design Patterns
This design paradigm is based on the principles of identifying, executing, and facilitating communication among various distinct entities. Behavioral patterns primarily focus on communication since they play a crucial role in ensuring that the different components maintain accurate synchronization of data. Consequently, this enhances the adaptability of interactions.
Let’s explore various classifications of behavioral design patterns.
Chain of Responsibility
This design pattern is primarily utilized to create a system in which each request travels through a sequence of events, being managed by various handlers. Requests are either processed and forwarded from one link in the chain to another or outright rejected. This design pattern is ideal for systems that require step-by-step verification when handling requests. For instance, consider an ATM. When we initiate a withdrawal request from the ATM, the machine evaluates the request and subsequently dispenses the requested amount using a combination of banknotes (Rs. 500, Rs. 200, Rs. 100).
var Request = function(amount) {
this.amount = amount;
console.log("Request Amount:" +this.amount);
}
Request.prototype = {
get: function(bill) {
var count = Math.floor(this.amount / bill);
this.amount -= count * bill;
console.log("Dispense " + count + " $" + bill + " bills");
return this;
}
}
function run() {
var request = new Request(378); //Requesting amount
request.get(100).get(50).get(20).get(10).get(5).get(1);
}
In this coding illustration, an instance of a request object is instantiated each time there is a withdrawal amount requested. This action triggers a sequence of method calls for the object, which are interconnected, with event handlers managing specific denominations. Ultimately, the ATM is able to release the requested assortment of banknotes that fulfill the processed request.
Observer Pattern
The observer pattern is categorized as a behavioral design pattern that primarily serves to establish a subscription framework. This framework enables the notification of multiple observers or entities regarding any events that take place. Consequently, this prompts event handlers to execute responses that are driven by those events. Commonly referred to as Pub/Sub, which stands for Publication/Subscription, this pattern involves a sequence of one-to-one dependencies between objects. The implementation of this design pattern fosters improved object-oriented design principles and facilitates a higher degree of loose coupling.
class Subject {
constructor () {
this.criticalNumber = 0
this.observers = []
}
addObserver (observer) {
this.observers.push(observer)
}
removeObserver (observer) {
let index = this.observers.findIndex(o => o === observer)
if (index !== -1) {
this.observers.splice(index, 1)
}
}
notify () {
console.log('Notifying observers about some important information')
this.observers.forEach (observer => {
observer.update(this.criticalNumber)
})
}
changeCriticalNumber () {
/* Changing the critical information */
this.criticalNumber = 42
/* Notifying the observers about this change */
this.notify()
}
}
class Observer {
constructor (id) {
this.id = id
}
update (criticalNumber) {
console.log(`Observer ${this.id} - Received an update from the subject ${criticalNumber}`)
}
}
In this illustration, we have developed a notification service framework designed to inform users regarding their subscriptions. Within this framework, we have established several objects and event listeners to track updates, referred to as subscribers. Consequently, we have defined two key classes: Subject and Observer. These classes are essential for maintaining vital information along with a registry of observers. When there is a change in the state of the critical information, the subject will alert all of its observers about the event through the notify method demonstrated in the preceding code. The observer class then executes an update method that processes notification requests from the subject.
Conclusion
In this tutorial, we examined a variety of prevalent design patterns utilized in JavaScript and how they effectively address both common and intricate challenges. Additionally, we discussed the primary classifications of design patterns, which include creational, structural, and behavioral design patterns. The creational patterns are primarily concerned with object-class structures to simplify the design process. Meanwhile, structural design patterns offer a more effective way to organize a problem and resolve it incrementally, whereas behavioral patterns facilitate the communication between different activities in a synchronized manner. We also provided a brief overview of several subcategories within these three main types of design patterns and their application in JavaScript. The implementation of design patterns in JavaScript is vital for application development. Each category pertains to distinct sets of challenges that may arise during the creation of software. However, these patterns guarantee that the systems we construct will achieve optimal performance due to the effective functionality inherent in these design patterns, which provide the most suitable solutions for every design issue encountered in the realm of JavaScript.