20
DecLearn JavaScript Design Patterns to Simplify Your Development
JavaScript Design Patterns
When you're working with JavaScript, you often need to structure your code to solve common problems efficiently. But what if you need to organize your code in a way that’s both reusable and scalable? That’s where design patterns come in. They act as blueprints for solving problems in a consistent way, making your code cleaner and more maintainable. Curious to see how they work? Let’s dive in and explore them step by step!
In this JavaScript tutorial, you’ll learn how JavaScript Design Patterns work and when to use them effectively. We’ll also go through examples to help you apply them to your own projects.
What are Design Patterns?
- Design patterns are like ready-made guides that help you solve common problems when writing programs. They show you how to solve something in a smart way that has worked well for others.
- It’s like having a recipe. For example, if you need to create objects in your program, you can follow the " Factory Method Design Pattern." It’s like saying, “Hey, instead of figuring out how to make every object from scratch, let me use a method that does it for me.”
- Using design patterns saves you time, makes your code clear, and helps you build software that is easy to fix or update later. It’s like helping to write better programs.
Types of Software Design Patterns in JavaScript
When you write programs in JavaScript, you can use Different Types of Design Patterns to solve problems. It’s like having different tools for different tasks. Below is a diagram that gives you a quick view of the main types.
1. Creational Patterns
Creational patterns are all about making objects in a smart way. Instead of creating objects directly, these patterns give you tools to manage object creation. It’s like having a factory that builds things for you, so you don’t have to worry about the details.
Here are some common creational patterns:
- Factory Pattern: It helps you create objects without specifying their exact class. It’s like saying, “I just need an object that works like this,” and the factory takes care of the rest.
- Singleton Pattern: This ensures you only have one instance of a class. It’s like having one manager who handles everything instead of multiple people doing the same job.
- Builder Pattern: It helps you construct complex objects step by step. It’s like assembling a car piece by piece instead of getting everything at once.
- Prototype Pattern: This lets you create new objects by copying an existing one. It’s like making a duplicate of a document instead of creating it from scratch.
Factory Pattern
The Factory Pattern allows you to create objects without specifying their exact class. It’s like having a factory that produces different objects depending on the need.
class Car {
constructor(type) {
this.type = type;
}
drive() {
return `${this.type} car is driving.`;
}
}
function CarFactory(type) {
return new Car(type);
}
// Usage
const sedan = CarFactory('Sedan');
console.log(sedan.drive()); // Sedan car is driving.
const suv = CarFactory('SUV');
console.log(suv.drive()); // SUV car is driving.
This Factory Pattern creates objects like "Sedan" or "SUV" without you needing to know the exact class each time.
Output
Sedan car is driving.
SUV car is driving.
Singleton Pattern
The Singleton Pattern ensures that only one instance of a class exists, regardless of how many times you try to create it.
class Manager {
constructor(name) {
if (Manager.instance) {
return Manager.instance;
}
this.name = name;
Manager.instance = this;
}
showManager() {
return `Manager: ${this.name}`;
}
}
// Usage
const manager1 = new Manager('Ravi');
console.log(manager1.showManager()); // Manager: Ravi
const manager2 = new Manager('Suresh');
console.log(manager2.showManager()); // Manager: Ravi (same instance)
In this example, no matter how many times you create an instance of Manager, you always get the same instance, as shown by the name "Ravi" staying the same.
Output
Manager: Ravi
Manager: Ravi
Builder Pattern
The Builder Pattern is used when creating complex objects step by step. It's like assembling a car piece by piece instead of getting everything at once.
class Car {
constructor() {
this.type = '';
this.color = '';
}
}
class CarBuilder {
setType(type) {
this.car.type = type;
return this;
}
setColor(color) {
this.car.color = color;
return this;
}
build() {
return this.car;
}
constructor() {
this.car = new Car();
}
}
// Usage
const builder = new CarBuilder();
const myCar = builder.setType('SUV').setColor('Red').build();
console.log(myCar); // Car { type: 'SUV', color: 'Red' }
This Builder Pattern allows you to build a car object step by step, setting each property individually like the type and color.
Output
Car { type: 'SUV', color: 'Red' }
Abstract Factory Pattern
The Abstract Factory Pattern is like a factory of factories. It helps you create families of related objects without specifying their concrete classes. Imagine you’re in a car showroom, and depending on the car type you want, you are directed to a specific factory that builds those cars.
class Sedan {
create() {
return "Sedan car created.";
}
}
class SUV {
create() {
return "SUV car created.";
}
}
class SedanFactory {
getCar() {
return new Sedan();
}
}
class SUVFactory {
getCar() {
return new SUV();
}
}
class CarFactory {
static getFactory(type) {
if (type === "Sedan") {
return new SedanFactory();
} else if (type === "SUV") {
return new SUVFactory();
}
return null;
}
}
// Usage
const sedanFactory = CarFactory.getFactory("Sedan");
const sedan = sedanFactory.getCar();
console.log(sedan.create()); // Sedan car created.
const suvFactory = CarFactory.getFactory("SUV");
const suv = suvFactory.getCar();
console.log(suv.create()); // SUV car created.
In this example, the CarFactory acts as an abstract factory, directing you to specific factories (like SedanFactory or SUVFactory) based on the type of car you need. Each factory then creates the specific car.
Output
Sedan car created.
SUV car created.
Prototype Pattern
The Prototype Pattern lets you create new objects by copying an existing one, avoiding the need to recreate it from scratch.
class Car {
constructor(type, color) {
this.type = type;
this.color = color;
}
clone() {
return new Car(this.type, this.color);
}
}
// Usage
const originalCar = new Car('Sedan', 'Blue');
const clonedCar = originalCar.clone();
console.log(originalCar); // Car { type: 'Sedan', color: 'Blue' }
console.log(clonedCar); // Car { type: 'Sedan', color: 'Blue' }
The Prototype Pattern lets you clone objects. In this case, the car is cloned with the same properties.
Output
Car { type: 'Sedan', color: 'Blue' }
Car { type: 'Sedan', color: 'Blue' }
Practice with these Articles: |
2. Structural Patterns
Structural patterns are all about how objects and classes are put together. They help you organize your code so it’s more efficient and easier to manage. It’s like arranging furniture in a room for maximum space and comfort.
Here are some common structural patterns:
- Adapter Pattern: It allows two incompatible interfaces to work together. It’s like a translator making two people who speak different languages understand each other.
- Decorator Pattern: This lets you add new functionality to an object without changing its structure. It’s like adding extra features to a car without redesigning it.
- Facade Pattern: It provides a simplified interface to a complex system. It’s like using a remote control to manage all the devices in your house without needing to understand how each one works.
- Composite Pattern: It allows you to compose objects into tree structures to represent part-whole hierarchies. It’s like organizing a company’s structure into departments and teams.
- Proxy Pattern: It controls access to an object, providing a placeholder for it. It’s like a receptionist who handles access to the CEO’s office.
Adapter Pattern
The Adapter Pattern is like a translator between two incompatible systems. It allows objects with different interfaces to work together.
class OldSystem {
request() {
return 'Request from Old System';
}
}
class NewSystem {
specificRequest() {
return 'Request from New System';
}
}
class Adapter {
constructor(newSystem) {
this.newSystem = newSystem;
}
request() {
return this.newSystem.specificRequest();
}
}
// Usage
const oldSystem = new OldSystem();
console.log(oldSystem.request()); // Request from Old System
const newSystem = new NewSystem();
const adapter = new Adapter(newSystem);
console.log(adapter.request()); // Request from New System
This Adapter Pattern allows an old system and a new system to work together by adapting the interface.
Output
Request from Old System
Request from New System
Decorator Pattern
The Decorator Pattern adds new functionality to an object without changing its original structure. It’s like customizing a car with new features without altering its design.
class Car {
drive() {
return 'Driving a car';
}
}
class SportsCar {
constructor(car) {
this.car = car;
}
drive() {
return `${this.car.drive()} fast!`;
}
}
class LuxuryCar {
constructor(car) {
this.car = car;
}
drive() {
return `${this.car.drive()} with luxury!`;
}
}
// Usage
const basicCar = new Car();
console.log(basicCar.drive()); // Driving a car
const sportsCar = new SportsCar(basicCar);
console.log(sportsCar.drive()); // Driving a car fast!
const luxuryCar = new LuxuryCar(basicCar);
console.log(luxuryCar.drive()); // Driving a car with luxury!
Here, we decorate the basic car with sports and luxury features using the Decorator Pattern.
Output
Driving a car
Driving a car fast!
Driving a car with luxury!
Facade Pattern
The Facade Pattern simplifies a complex system by providing a unified interface. It’s like using a remote control to control multiple devices at once.
class Engine {
start() {
return 'Engine started';
}
}
class Lights {
turnOn() {
return 'Lights are on';
}
}
class CarFacade {
constructor() {
this.engine = new Engine();
this.lights = new Lights();
}
startCar() {
console.log(this.engine.start());
console.log(this.lights.turnOn());
}
}
// Usage
const carFacade = new CarFacade();
carFacade.startCar();
The Facade Pattern simplifies the process of starting the car by combining actions into a single method.
Output
Engine started
Lights are on
Composite Pattern
The Composite Pattern allows you to treat individual objects and compositions of objects in the same way. It’s like organizing employees into departments and teams.
class Employee {
constructor(name) {
this.name = name;
}
show() {
return this.name;
}
}
class Department {
constructor(name) {
this.name = name;
this.employees = [];
}
add(employee) {
this.employees.push(employee);
}
show() {
return `${this.name}: ${this.employees.map(emp => emp.show()).join(', ')}`;
}
}
// Usage
const emp1 = new Employee('Amit');
const emp2 = new Employee('Ravi');
const dept = new Department('Development');
dept.add(emp1);
dept.add(emp2);
console.log(dept.show()); // Development: Amit, Ravi
The Composite Pattern allows us to treat individual employees and entire departments similarly, making it easier to manage them.
Output
Development: Amit, Ravi
Proxy Pattern
The Proxy Pattern controls access to an object by providing a placeholder. It’s like a receptionist managing access to an office.
class RealSubject {
request() {
return 'Request made to real subject';
}
}
class Proxy {
constructor(realSubject) {
this.realSubject = realSubject;
}
request() {
console.log('Proxy: Handling the request');
return this.realSubject.request();
}
}
// Usage
const realSubject = new RealSubject();
const proxy = new Proxy(realSubject);
console.log(proxy.request()); // Proxy: Handling the request
The Proxy Pattern allows a "proxy" object to control access to the real object, managing requests before passing them on.
Output
Proxy: Handling the request
Request made to real subject
Practice with these Articles: |
3. Behavioral Patterns
Behavioral patterns focus on how objects interact and communicate with each other. These patterns help you manage algorithms, responsibilities, and communication between objects in a program. It’s like organizing a team where each member has a specific role to play, and they work together smoothly.
Here are some common behavioral patterns:
- Observer Pattern: It allows an object to notify other objects about changes without knowing who or what those objects are. It’s like a teacher announcing updates to the class, and the students can decide whether they want to listen or not.
- Strategy Pattern: This allows a class to choose a behavior at runtime. It’s like a restaurant offering a variety of dishes, and you can choose which one you want depending on your mood.
- Command Pattern: It turns a request into a stand-alone object. It’s like creating a task list, where each task can be executed later.
- Chain of Responsibility Pattern: It allows a request to be passed along a chain of handlers until one of them handles it. It’s like a customer service desk where each representative can handle different issues, and if one can’t, they pass it along to the next person.
- State Pattern: It lets an object change its behavior when its internal state changes. It’s like a character in a game who acts differently based on whether they are in normal or combat mode.
Read More: Java Class and Object |
Observer Pattern
The Observer Pattern is like a newsletter subscription. When the subject (e.g., a news publisher) updates its information, all the subscribers (observers) are notified automatically.
class Publisher {
constructor() {
this.subscribers = [];
}
subscribe(subscriber) {
this.subscribers.push(subscriber);
}
unsubscribe(subscriber) {
this.subscribers = this.subscribers.filter(sub => sub !== subscriber);
}
notify() {
this.subscribers.forEach(subscriber => subscriber.update());
}
}
class Subscriber {
constructor(name) {
this.name = name;
}
update() {
console.log(`${this.name} has been notified.`);
}
}
// Usage
const publisher = new Publisher();
const subscriber1 = new Subscriber('Amit');
const subscriber2 = new Subscriber('Ravi');
publisher.subscribe(subscriber1);
publisher.subscribe(subscriber2);
publisher.notify(); // Both subscribers will be notified
The Publisher updates all its subscribers when something changes.
Output
Amit has been notified.
Ravi has been notified.
Strategy Pattern
The Strategy Pattern allows you to choose a strategy for an object to use at runtime. It’s like a business offering different marketing strategies depending on the situation.
class PaymentStrategy {
pay(amount) {
throw 'This method should be overridden!';
}
}
class CreditCardPayment extends PaymentStrategy {
pay(amount) {
console.log(`Paying ${amount} using Credit Card`);
}
}
class PayPalPayment extends PaymentStrategy {
pay(amount) {
console.log(`Paying ${amount} using PayPal`);
}
}
class ShoppingCart {
constructor(paymentMethod) {
this.paymentMethod = paymentMethod;
}
checkout(amount) {
this.paymentMethod.pay(amount);
}
}
// Usage
const cart1 = new ShoppingCart(new CreditCardPayment());
cart1.checkout(100); // Paying 100 using Credit Card
const cart2 = new ShoppingCart(new PayPalPayment());
cart2.checkout(200); // Paying 200 using PayPal
The Strategy Pattern allows the shopping cart to use different payment methods without changing its structure.
Output
Paying 100 using Credit Card
Paying 200 using PayPal
Command Pattern
The Command Pattern encapsulates a request as an object. It’s like creating a to-do list where each item is an independent command.
class Command {
execute() {
throw 'This method should be overridden!';
}
}
class LightOnCommand extends Command {
constructor(light) {
super();
this.light = light;
}
execute() {
this.light.turnOn();
}
}
class Light {
turnOn() {
console.log('Light is ON');
}
turnOff() {
console.log('Light is OFF');
}
}
class RemoteControl {
pressButton(command) {
command.execute();
}
}
// Usage
const light = new Light();
const lightOn = new LightOnCommand(light);
const remoteControl = new RemoteControl();
remoteControl.pressButton(lightOn); // Light is ON
In the Command Pattern, we encapsulate a request (turning on the light) into an object and then execute it later.
Output
Light is ON
Chain of Responsibility Pattern
The Chain of Responsibility Pattern allows requests to be passed along a chain of handlers. It’s like a helpdesk where different representatives handle different types of issues.
class Handler {
setNext(handler) {
this.nextHandler = handler;
}
handle(request) {
if (this.nextHandler) {
this.nextHandler.handle(request);
} else {
console.log('End of the chain, no handler found for:', request);
}
}
}
class ConcreteHandler1 extends Handler {
handle(request) {
if (request === 'request1') {
console.log('Handler1 handled request1');
} else {
super.handle(request);
}
}
}
class ConcreteHandler2 extends Handler {
handle(request) {
if (request === 'request2') {
console.log('Handler2 handled request2');
} else {
super.handle(request);
}
}
}
// Usage
const handler1 = new ConcreteHandler1();
const handler2 = new ConcreteHandler2();
handler1.setNext(handler2);
handler1.handle('request1'); // Handler1 handled request1
handler1.handle('request2'); // Handler2 handled request2
handler1.handle('request3'); // End of the chain, no handler found for: request3
The Chain of Responsibility Pattern helps pass requests along a chain of handlers until one can process it.
Output
Handler1 handled request1
Handler2 handled request2
End of the chain, no handler found for: request3
State Pattern
The State Pattern allows an object to change its behavior based on its internal state. It’s like a character in a game that behaves differently in different modes.
class Character {
constructor() {
this.state = 'normal';
}
setState(state) {
this.state = state;
}
attack() {
if (this.state === 'normal') {
console.log('Attacking normally');
} else if (this.state === 'combat') {
console.log('Attacking aggressively');
}
}
}
// Usage
const character = new Character();
character.attack(); // Attacking normally
character.setState('combat');
character.attack(); // Attacking aggressively
The State Pattern lets an object change its behavior when its internal state changes, similar to how a character behaves differently based on their mode.
Output
Attacking normally
Attacking aggressively
Practice with these Articles: |
When to Use Design Patterns in JavaScript
You should use design patterns in JavaScript when you encounter common problems in your code that require efficient, reusable solutions. Design patterns help make your code cleaner, easier to maintain, and more flexible.
Here are a few situations when design patterns can be helpful for you:
- When you need to create objects in a flexible way: For example, if you want to create many similar objects but don’t want to repeat the same code each time, you can use the Factory Pattern.
- When you need to share data between different parts of your program: The Singleton Pattern ensures only one instance of a class is used across your app, making data sharing easy.
- When you need to add features without changing existing code: The Decorator Pattern allows you to add new features to an object without altering its original code. This is useful for adding new functionality to existing systems.
- When you need to separate concerns in large applications: The Observer Pattern helps manage how different parts of your program communicate without directly interacting with each other.
When Not to Use Design Patterns in JavaScript
You should avoid using design patterns in JavaScript when they add unnecessary complexity or overhead to your code. While design patterns are helpful in many situations, it's important to recognize when they’re not needed. Here are a few situations when you might want to skip using design patterns:
- When your code is simple and doesn’t require abstraction: If the problem you're solving is straightforward, adding a design pattern may make your code more complicated than it needs to be.
- When it introduces unnecessary overhead: Some design patterns, like the Singleton or Observer patterns, can introduce additional complexity that slows down development, especially if they’re not necessary for your specific use case.
- When it doesn’t fit the problem at hand: Not every design pattern is suited for every situation. If the pattern doesn’t align with your goals or the structure of your application, it’s better to stick with simpler solutions.
- When you’re over-engineering: Sometimes, developers try to apply design patterns in situations where they’re not required. Over-engineering can lead to unnecessary abstraction, making the code harder to understand and maintain.
Read More: Java Abstraction |
Summary
This article covered the essentials of JavaScript design patterns, including when and why to use them. It explained how design patterns help solve common coding problems by providing efficient, reusable solutions. The article also discussed situations where JavaScript design patterns can be helpful and when it’s best to avoid them to keep your code simple and maintainable. By understanding these patterns, you can write cleaner, more scalable JavaScript code.
Want to learn more about JavaScript? Sign up for the ScholarHat JavaScript Programming Course today and become a JavaScript pro! Don't miss your chance to improve your skills and take your career to the next level!
FAQs
Take our Javascript skill challenge to evaluate yourself!
In less than 5 minutes, with our skill challenge, you can identify your knowledge gaps and strengths in a given skill.