Imagine a group of architects tasked with designing a skyscraper. During the design stage, they face numerous considerations, such as:
- Architectural Style: Should the building be brutalist, minimalist, or take on another design?
- Width of the Base: What dimensions are required to prevent collapse on windy days?
- Protection Against Natural Disasters: What structural measures must be in place to guard against earthquakes, flooding, and other natural threats?
The array of factors to consider is vast, but one thing is clear: a blueprint is necessary to guide the construction of the skyscraper. Without a cohesive design plan, architects would have to reinvent the wheel, leading to confusion and inefficiency.
In the programming realm, developers often rely on design patterns to create software that adheres to clean code principles. These patterns are prevalent, allowing programmers to concentrate on shipping new features rather than repeatedly solving the same problems.
In this article, you will discover several common JavaScript design patterns, accompanied by practical Node.js projects to illustrate each pattern's application.
What Are Design Patterns in Software Engineering?
Design patterns serve as pre-made blueprints that developers can adapt to tackle recurring design problems in coding. It’s important to note that these patterns are not merely code snippets; they represent general concepts for addressing challenges.
Benefits of Design Patterns:
Tried and Tested: Design patterns effectively solve numerous issues in software design. By knowing and applying these patterns, developers can leverage object-oriented design principles to tackle various problems.
Common Language: Design patterns facilitate efficient communication among team members. For instance, if a teammate suggests using the factory method, everyone understands the implication and rationale behind the recommendation.
In this article, we will explore three categories of design patterns:
- Creational: Used for creating objects.
- Structural: Focused on assembling these objects into a functional structure.
- Behavioral: Concerned with the responsibilities assigned to those objects.
Let’s dive into these design patterns in action!
Creational Design Patterns
Creational patterns encompass various techniques that assist developers in object creation.
Factory Pattern
The factory method is a pattern for creating objects that allows for greater control over the object creation process. This method is ideal when you want to centralize the logic for object instantiation.
// File: factory-pattern.js
const createCar = ({ company, model, size }) => ({
company,
model,
size,
showDescription() {
console.log(
"The all new ",
model,
" is built by ",
company,
" and has an engine capacity of ",
size,
" CC "
);
},
});
const challenger = createCar({
company: "Dodge",
model: "Challenger",
size: 6162,
});
challenger.showDescription();
Breakdown:
- The
createCar
function serves as an interface for generating Car objects. - Each Car has properties:
company
,model
, andsize
, alongside ashowDescription
method. - We instantiate a
challenger
object and invoke itsshowDescription
method to display its attributes.
Builder Pattern
The builder method enables step-by-step construction of objects, allowing for flexibility in creating objects with only necessary functions.
// File: builder-pattern.js
class Car {
constructor({ model, company, size }) {
this.model = model;
this.company = company;
this.size = size;
}
}
Car.prototype.showDescription = function () {
console.log(
this.model +
" is made by " +
this.company +
" and has an engine capacity of " +
this.size +
" CC "
);
};
Car.prototype.reduceSize = function () {
this.size -= 2; // Reduce engine size
};
const challenger = new Car({
company: "Dodge",
model: "Challenger",
size: 6162,
});
challenger.showDescription();
console.log('reducing size...');
challenger.reduceSize();
challenger.reduceSize();
challenger.showDescription();
Explanation:
- We define a
Car
class to instantiate objects and extend it using prototypes for added functionalities. - After instantiating the
challenger
object, we log its details before and after reducing its size.
Structural Design Patterns
Structural design patterns focus on how various components of our program collaborate.
Adapter Pattern
The adapter pattern allows objects with incompatible interfaces to work together. This is particularly useful when adapting old code to a new codebase without causing breaking changes.
// File: adapter-pattern.js
const groupsWithSoldAlbums = [
{ name: "Twice", sold: 23 },
{ name: "Blackpink", sold: 23 },
{ name: "Aespa", sold: 40 },
{ name: "NewJeans", sold: 45 },
];
console.log("Before:", groupsWithSoldAlbums);
let illit = { name: "Illit", revenue: 300 };
const COST_PER_ALBUM = 30;
const convertToAlbumsSold = (group) => {
return { name: group.name, sold: parseInt(group.revenue / COST_PER_ALBUM) };
};
illit = convertToAlbumsSold(illit);
groupsWithSoldAlbums.push(illit);
console.log("After:", groupsWithSoldAlbums);
Overview:
- An array
groupsWithSoldAlbums
stores objects withname
andsold
properties. - We create an
illit
object withrevenue
and convert it using theconvertToAlbumsSold
adapter function to maintain compatibility.
Decorator Pattern
The decorator design pattern allows the addition of new methods and properties to objects after creation, enabling runtime capabilities.
// File: decorator-pattern.js
class MusicArtist {
constructor({ name, members }) {
this.name = name;
this.members = members;
}
displayMembers() {
console.log("Group name", this.name, " has", this.members.length, " members:");
this.members.forEach((item) => console.log(item));
}
}
class PerformingArtist extends MusicArtist {
constructor({ name, members, eventName, songName }) {
super({ name, members });
this.eventName = eventName;
this.songName = songName;
}
perform() {
console.log(
this.name + " is now performing at " + this.eventName + " with their hit song " + this.songName
);
}
}
const akmu = new PerformingArtist({
name: "Akmu",
members: ["Suhyun", "Chanhyuk"],
eventName: "MNET",
songName: "Hero",
});
akmu.displayMembers();
akmu.perform();
Summary:
- The
MusicArtist
class defines the base structure, whilePerformingArtist
extends it to introduce new properties and methods. - We instantiate the
akmu
object and log its details along with the performance information.
Behavioral Design Patterns
This category focuses on the interaction and communication between different components within a program.
Chain of Responsibility
The Chain of Responsibility design pattern allows requests to pass through a chain of components until one is found capable of processing it.
// File: chain-of-responsibility-pattern.js
class Leader {
constructor(responsibility, name) {
this.responsibility = responsibility;
this.name = name;
}
setNext(handler) {
this.nextHandler = handler;
return handler;
}
handle(responsibility) {
if (this.nextHandler) {
console.log(this.name + " cannot handle operation: " + responsibility);
return this.nextHandler.handle(responsibility);
}
}
}
// Example usage of the Chain of Responsibility
const leader1 = new Leader("manage budget", "Alice");
const leader2 = new Leader("approve project", "Bob");
leader1.setNext(leader2);
leader1.handle("approve project");
Explanation:
- The
Leader
class processes responsibilities, passing requests along the chain until a capable handler is found. - In the example, if
Alice
cannot manage the operation, the request is forwarded toBob
.
Conclusion
Design patterns serve as essential tools for software developers, providing tested solutions for common programming challenges. By understanding and applying these patterns, developers can create more efficient, scalable, and maintainable code. The next time you're faced with a coding challenge, remember that a well-established design pattern might just be the blueprint you need!
Top comments (0)