Essential JavaScript Design Patterns for Modern Development

Essential JavaScript Design Patterns for Modern Development

30 Sep 2024
Beginner
944 Views
99 min read
Learn with an interactive course and practical hands-on labs

⭐ .NET Design Patterns Course: Design Patterns in C# Online Training

Design Patterns in JavaScript

Design patterns in JavaScript are an important concept in software development. They provide tested answers to common design issues, helping developers reduce code structure and improve maintainability. Design patterns allow you to construct resilient and scalable systems while also supporting excellent software architectural practices.

In the Design Pattern tutorial, we'll learn about what are design patterns in JavaScript, the types of design patterns in JavaScript, when to use them, and how they might help you write better code. Let us begin by examining "What are Design Patterns?"

What are Design Patterns?

  • A design pattern is a general, recurring solution to a commonly encountered problem in software design that is utilized in software engineering.
  • It is not a comprehensive design that can be implemented in code straight immediately.
  • It is a description or model for issue solving that may be used in a variety of settings.
  • Design patterns are similar to cooking recipes in that they provide a tried-and-true approach to producing consistent results without having to start from scratch each time.
Read More:
Different Types of Software Design Principles
What is an IoC Container or DI Container

Types of Design Patterns

There are three types of Design Patterns that we have:
  1. Creational Patterns
  2. Structural Patterns
  3. Behavioral Patterns
Now, we will discuss each of them step by step:

1. Creational Design Patterns

  • Creational design patterns are one category of design patterns used in software development.
  • They work on improving the flexibility and efficiency of the object production process.
  • Creational design patterns define ways to create objects.
Read More: 7 Phases of the Software Development Life Cycle

Creational Design Patterns

  • The aim of these design patterns is not to compose objects directly but to introduce different ways for object creation so as to make the code easier and more flexible.
  • Using creational design patterns, developers can maintain the creation process in one place, separately from other code, thus enabling easy maintenance and upgrade of a program.
  • They basically help in building systems where the way things are created does not interfere with the overall design.

There are five types of creational design patterns:

  1. Singleton Design Pattern
  2. Factory Method Design Pattern
  3. Abstract Factory Design Pattern
  4. Builder Design Pattern
  5. Prototype Design Pattern

Let's start with a singleton design pattern.

1. Singleton design pattern

  • Singleton Design Pattern ensures that a class has only one instance throughout the application and provides a global point to access it.
  • This is useful when you need exactly one object, such as a configuration manager, to coordinate actions across the system.

Example

 // Singleton class
class ScholarHatSingleton {
    // Private static variable to hold the single instance of the class
    static instance = null;

    // Private constructor to prevent instantiation from outside the class
    constructor() {
        if (ScholarHatSingleton.instance) {
            throw new Error("Use ScholarHatSingleton.getInstance() to get the single instance.");
        }
    }

    // Public static method to provide access to the single instance of the class
    static getInstance() {
        // Check if instance is null; if it is, create a new instance
        if (!ScholarHatSingleton.instance) {
            ScholarHatSingleton.instance = new ScholarHatSingleton();
        }
        // Return the single instance
        return ScholarHatSingleton.instance;
    }

    // Method to display a message
    showMessage() {
        console.log("Welcome to ScholarHat Singleton (JavaScript)!");
    }
}

// Example usage
const singleton = ScholarHatSingleton.getInstance();
singleton.showMessage();  

Output

  Welcome to ScholarHat Singleton (JavaScript)!

Explanation

  • Singleton Design: Ensures a single instance of ScholarHatSingleton using a private constructor and static method.
  • Instance Management: Uses a static variable to hold the instance, creating it only when needed.
  • Functionality: Includes a method (showMessage()) to demonstrate actions performed by the instance.

2. Factory Method Design Pattern

  • Factory Method Design Pattern defines a method for creating objects but lets subclasses decide which class to instantiate.
  • This pattern is useful when you need to create objects from a family of related classes. It allows you to use a common interface while delegating the actual creation to subclasses.
  • For example, a document editor may use a factory method to create different types of documents (e.g., text, spreadsheet) based on user input without knowing the specific class of the document being created.

Example

  // Course interface (simulated using abstract class in JavaScript)
class Course {
    getCourseDetails() {
        throw new Error("This method should be overridden by subclasses.");
    }
}

// JavaScript course implementation
class JavaScriptCourse extends Course {
    getCourseDetails() {
        console.log("This is a JavaScript course.");
    }
}

// Python course implementation
class PythonCourse extends Course {
    getCourseDetails() {
        console.log("This is a Python course.");
    }
}

// Course factory
class CourseFactory {
    static getCourse(courseType) {
        if (!courseType) {
            return null;
        }
        if (courseType.toUpperCase() === "JAVASCRIPT") {
            return new JavaScriptCourse();
        } else if (courseType.toUpperCase() === "PYTHON") {
            return new PythonCourse();
        }
        return null;
    }
}

// Example usage
const jsCourse = CourseFactory.getCourse("JAVASCRIPT");
jsCourse?.getCourseDetails();

const pythonCourse = CourseFactory.getCourse("PYTHON");
pythonCourse?.getCourseDetails();   

Output

This is a JavaScript course.
This is a Python course.

Explanation

  • Abstract Class: Defines a base Course class with a method to be overridden by specific course classes.
  • Factory Method: Implements CourseFactory to create instances of JavaScriptCourse and PythonCourse based on input.
  • Usage: Shows how to create and display course details using the factory.

3. Abstract Factory design Pattern

  • Abstract Factory Design Pattern provides an interface for creating families of related objects without specifying their concrete classes.
  • This pattern is useful when you need to create objects that are part of a larger group, ensuring that the objects fit together well.

Example

  // Abstract Product Interfaces
class Course {
    getCourseDetails() {
        throw new Error("This method should be overridden by subclasses.");
    }
}

class Certification {
    getCertificationDetails() {
        throw new Error("This method should be overridden by subclasses.");
    }
}

// Concrete Products for JavaScript
class JavaScriptCourse extends Course {
    getCourseDetails() {
        console.log("ScholarHat offers an in-depth JavaScript course.");
    }
}

class JavaScriptCertification extends Certification {
    getCertificationDetails() {
        console.log("ScholarHat provides JavaScript Certification after course completion.");
    }
}

// Concrete Products for Python
class PythonCourse extends Course {
    getCourseDetails() {
        console.log("ScholarHat offers a comprehensive Python course.");
    }
}

class PythonCertification extends Certification {
    getCertificationDetails() {
        console.log("ScholarHat provides Python Certification after course completion.");
    }
}

// Abstract Factory Interface
class CourseFactory {
    createCourse() {
        throw new Error("This method should be overridden by subclasses.");
    }

    createCertification() {
        throw new Error("This method should be overridden by subclasses.");
    }
}

// Concrete Factories
class JavaScriptFactory extends CourseFactory {
    createCourse() {
        return new JavaScriptCourse();
    }

    createCertification() {
        return new JavaScriptCertification();
    }
}

class PythonFactory extends CourseFactory {
    createCourse() {
        return new PythonCourse();
    }

    createCertification() {
        return new PythonCertification();
    }
}

// Example usage
const javaScriptFactory = new JavaScriptFactory();
const javaScriptCourse = javaScriptFactory.createCourse();
const javaScriptCertification = javaScriptFactory.createCertification();
javaScriptCourse.getCourseDetails();
javaScriptCertification.getCertificationDetails();

const pythonFactory = new PythonFactory();
const pythonCourse = pythonFactory.createCourse();
const pythonCertification = pythonFactory.createCertification();
pythonCourse.getCourseDetails();
pythonCertification.getCertificationDetails();   

Output

ScholarHat offers an in-depth JavaScript course.
ScholarHat provides JavaScript Certification after course completion.
ScholarHat offers a comprehensive Python course.
ScholarHat provides Python Certification after course completion. 

Explanation

  • Abstract Products: Defines abstract classes for Course and Certification, with concrete classes for JavaScript and Python.
  • Factory Pattern: Implements an abstract factory (CourseFactory) with specific factories for creating course and certification objects.
  • Demonstration: Shows how to create and display details for JavaScript and Python courses and certifications using the factories.

4. Builder Design Pattern

  • Builder Pattern separates the process of constructing a complex object from its representation, allowing you to build different types of objects using the same construction process.
  • This pattern is helpful when creating complex objects with many optional parts, making the code cleaner and more manageable.

Example

// ScholarHatCourse Class
class ScholarHatCourse {
    constructor(builder) {
        this.name = builder.name;
        this.duration = builder.duration;
        this.level = builder.level;
    }

    // Getters
    getName() {
        return this.name;
    }

    getDuration() {
        return this.duration;
    }

    getLevel() {
        return this.level;
    }

    // Static Builder Class
    static CourseBuilder = class {
        constructor() {
            this.name = '';
            this.duration = '';
            this.level = '';
        }

        setName(name) {
            this.name = name;
            return this;
        }

        setDuration(duration) {
            this.duration = duration;
            return this;
        }

        setLevel(level) {
            this.level = level;
            return this;
        }

        build() {
            return new ScholarHatCourse(this);
        }
    };
}

// Main Function
function builderPatternExample() {
    // Creating a ScholarHat course using the Builder Pattern
    const javascriptCourse = new ScholarHatCourse.CourseBuilder()
        .setName("JavaScript Programming")
        .setDuration("6 months")
        .setLevel("Intermediate")
        .build();

    // Displaying course details
    console.log("Course Name: " + javascriptCourse.getName());
    console.log("Duration: " + javascriptCourse.getDuration());
    console.log("Level: " + javascriptCourse.getLevel());

    // Creating another course
    const pythonCourse = new ScholarHatCourse.CourseBuilder()
        .setName("Python Programming")
        .setDuration("4 months")
        .setLevel("Beginner")
        .build();

    // Displaying course details
    console.log("\nCourse Name: " + pythonCourse.getName());
    console.log("Duration: " + pythonCourse.getDuration());
    console.log("Level: " + pythonCourse.getLevel());
}

// Calling the main function
builderPatternExample();   

Output

Course Name: JavaScript Programming
Duration: 6 months
Level: Intermediate

Course Name: Python Programming
Duration: 4 months
Level: Beginner

Explanation

  • Builder Pattern: ScholarHatCourse uses a CourseBuilder for flexible course creation with attributes like name, duration, and level.
  • Fluent Interface: Methods in CourseBuilder return this for chaining, enabling easy and readable course configuration.
  • Usage Example: The builderPatternExample function showcases creating and displaying two courses: JavaScript and Python.

5. Prototype Design Pattern

  • Prototype Pattern creates new objects by copying an existing object, known as the prototype, rather than building them from scratch.
  • This is useful when object creation is costly or complex, allowing for faster and more efficient object creation.

Example

// Abstract Prototype Class
class ScholarHatCourse {
    constructor() {
        this.courseName = '';
        this.courseLevel = '';
    }

    // Getters and Setters
    getCourseName() {
        return this.courseName;
    }

    setCourseName(name) {
        this.courseName = name;
    }

    getCourseLevel() {
        return this.courseLevel;
    }

    setCourseLevel(level) {
        this.courseLevel = level;
    }

    // Abstract clone method (should be overridden by concrete classes)
    clone() {
        throw new Error("Clone method should be implemented by subclass");
    }

    // Method to display course details
    showCourseDetails() {
        console.log("Course Name: " + this.courseName);
        console.log("Course Level: " + this.courseLevel);
    }
}

// Concrete Prototype Class for JavaScript Course
class JavaScriptCourse extends ScholarHatCourse {
    constructor() {
        super();
        this.setCourseName("JavaScript Programming");
        this.setCourseLevel("Intermediate");
    }

    // Overriding clone method
    clone() {
        return Object.assign(Object.create(Object.getPrototypeOf(this)), this);
    }
}

// Concrete Prototype Class for Python Course
class PythonCourse extends ScholarHatCourse {
    constructor() {
        super();
        this.setCourseName("Python Programming");
        this.setCourseLevel("Beginner");
    }

    // Overriding clone method
    clone() {
        return Object.assign(Object.create(Object.getPrototypeOf(this)), this);
    }
}

// Main Function
function prototypePatternExample() {
    // Creating original JavaScript course
    const javascriptCourse = new JavaScriptCourse();
    javascriptCourse.showCourseDetails();

    // Cloning the JavaScript course
    const clonedJavaScriptCourse = javascriptCourse.clone();
    clonedJavaScriptCourse.showCourseDetails();

    // Creating original Python course
    const pythonCourse = new PythonCourse();
    pythonCourse.showCourseDetails();

    // Cloning the Python course
    const clonedPythonCourse = pythonCourse.clone();
    clonedPythonCourse.showCourseDetails();
}

// Calling the main function
prototypePatternExample();   

Output

Course Name: JavaScript Programming
Course Level: Intermediate
Course Name: JavaScript Programming
Course Level: Intermediate
Course Name: Python Programming
Course Level: Beginner
Course Name: Python Programming
Course Level: Beginner  

Explanation

  • Prototype Pattern: The ScholarHatCourse class serves as an abstract prototype, defining common properties and a cloning method for course objects.
  • Concrete Classes: JavaScriptCourse and PythonCourse extend ScholarHatCourse, implementing the clone method to create copies of course instances.
  • Example Usage: The prototypePatternExample function demonstrates creating original course objects, cloning them, and displaying their details.

2. Structural Patterns

  • Structural design patterns are a type of design pattern in software development that focuses on combining classes or objects to construct larger, more complicated structures.

Structural Patterns

  • They aid in organizing and managing object interactions in software systems, resulting in increased flexibility, reusability, and maintainability.

There are seven types of Structural design patterns.

  1. Adapter Design Pattern
  2. Bridge Design Pattern
  3. Composite Design Pattern
  4. Decorator Design Pattern
  5. Facade Design Pattern
  6. Flyweight Design Pattern
  7. Proxy Design Pattern
Let's explore it one by one:

1. Adapter Pattern Design Pattern

  • Adapter Pattern allows incompatible interfaces to work together by wrapping one interface with another.
  • This pattern is like a translator that enables two systems with different languages to communicate seamlessly.

Example

// Target Interface
class IScholarHatCourse {
    getCourseDetails() {
        throw new Error("This method should be implemented by subclasses");
    }
}

// Adaptee Class (Existing Class)
class PythonCourseDetails {
    displayCourseDetails() {
        console.log("ScholarHat offers a comprehensive Python course.");
    }
}

// Adapter Class
class PythonCourseAdapter extends IScholarHatCourse {
    constructor(pythonCourseDetails) {
        super();
        this._pythonCourseDetails = pythonCourseDetails;
    }

    // Implementing the target interface method
    getCourseDetails() {
        this._pythonCourseDetails.displayCourseDetails();
    }
}

// Main Function
function adapterPatternExample() {
    // Existing Python course details (Adaptee)
    const pythonDetails = new PythonCourseDetails();

    // Adapter makes PythonCourseDetails compatible with IScholarHatCourse interface
    const courseAdapter = new PythonCourseAdapter(pythonDetails);

    // Using the adapter to get course details
    courseAdapter.getCourseDetails();
}

// Calling the main function
adapterPatternExample();   

Output

  ScholarHat offers a comprehensive Python course.

Explanation

  • Adapter Pattern: IScholarHatCourse is the target interface, while PythonCourseDetails is the existing class with a method for course details.
  • Adapter Implementation: PythonCourseAdapter adapts PythonCourseDetails to the IScholarHatCourse interface by implementing the getCourseDetails method.
  • Example Usage: The adapter is used to display course information via the getCourseDetails method in the adapterPatternExample function.

2. Bridge Pattern Design Pattern

  • A Bridge Pattern separates an object’s abstraction from its implementation, allowing it to vary independently.
  • This pattern is useful when you want to decouple a system so that both the abstraction and implementation can evolve without affecting each other.

Example

// Implementor Interface
class ICourseContent {
    getContent() {
        throw new Error("This method should be implemented by subclasses");
    }
}

// Concrete Implementor 1
class VideoContent extends ICourseContent {
    getContent() {
        console.log("ScholarHat provides video content for the course.");
    }
}

// Concrete Implementor 2
class ArticleContent extends ICourseContent {
    getContent() {
        console.log("ScholarHat provides article content for the course.");
    }
}

// Abstraction
class ScholarHatCourse {
    constructor(content) {
        this.content = content;
    }

    showCourseDetails() {
        throw new Error("This method should be implemented by subclasses");
    }
}

// Refined Abstraction 1 (JavaScript Course)
class JavaScriptCourse extends ScholarHatCourse {
    showCourseDetails() {
        console.log("JavaScript Programming Course:");
        this.content.getContent();
    }
}

// Refined Abstraction 2 (Python Course)
class PythonCourse extends ScholarHatCourse {
    showCourseDetails() {
        console.log("Python Programming Course:");
        this.content.getContent();
    }
}

// Main Function
function bridgePatternExample() {
    // Creating a JavaScript course with video content
    const javascriptCourse = new JavaScriptCourse(new VideoContent());
    javascriptCourse.showCourseDetails();

    // Creating a Python course with article content
    const pythonCourse = new PythonCourse(new ArticleContent());
    pythonCourse.showCourseDetails();
}

// Calling the main function
bridgePatternExample();   

Output

JavaScript Programming Course:
ScholarHat provides video content for the course.
Python Programming Course:
ScholarHat provides article content for the course.

Explanation

  • Bridge Pattern: Defines ICourseContent for content types and implements VideoContent and ArticleContent.
  • Course Abstraction: ScholarHatCourse is an abstract class, with JavaScriptCourse and PythonCourse detailing how to show course content.
  • Usage Example: The bridgePatternExample function creates courses with different content types, showcasing the bridge pattern's flexibility.

3. Composite Pattern Design Pattern

  • Composite Pattern lets you compose objects into tree structures to represent part-whole hierarchies.
  • This pattern allows clients to treat individual objects and compositions of objects uniformly, simplifying the handling of complex structures.

Example

// Component Interface
class IScholarHatCourse {
    showCourseDetails() {
        throw new Error("This method should be implemented by subclasses");
    }
}

// Leaf Class 1
class SingleCourse extends IScholarHatCourse {
    constructor(courseName) {
        super();
        this.courseName = courseName;
    }

    showCourseDetails() {
        console.log("Course: " + this.courseName);
    }
}

// Composite Class
class CourseBundle extends IScholarHatCourse {
    constructor() {
        super();
        this.courses = [];
    }

    addCourse(course) {
        this.courses.push(course);
    }

    removeCourse(course) {
        const index = this.courses.indexOf(course);
        if (index > -1) {
            this.courses.splice(index, 1);
        }
    }

    showCourseDetails() {
        for (let course of this.courses) {
            course.showCourseDetails();
        }
    }
}

// Main Function
function compositePatternExample() {
    // Single courses (Leafs)
    const javascriptCourse = new SingleCourse("JavaScript Programming");
    const pythonCourse = new SingleCourse("Python Programming");
    const dataScienceCourse = new SingleCourse("Data Science");

    // Course bundle (Composite)
    const programmingBundle = new CourseBundle();
    programmingBundle.addCourse(javascriptCourse);
    programmingBundle.addCourse(pythonCourse);

    // Full bundle including programming and data science courses
    const fullBundle = new CourseBundle();
    fullBundle.addCourse(programmingBundle);
    fullBundle.addCourse(dataScienceCourse);

    // Display details of the full bundle
    console.log("ScholarHat Full Course Bundle:");
    fullBundle.showCourseDetails();
}

// Calling the main function
compositePatternExample();  

Output

ScholarHat Full Course Bundle:
Course: JavaScript Programming
Course: Python Programming
Course: Data Science

Explanation

  • Interface and Classes: Defines IScholarHatCourse for course representation, with SingleCourse for individual courses and CourseBundle for multiple courses.
  • Composite Management: CourseBundle manages adding/removing courses and displays details for all included courses.
  • Example Usage: Demonstrates creating individual courses and a composite bundle, showing aggregated course details.

4. Decorator Pattern Design Pattern

  • Decorator Pattern adds additional responsibilities to an object dynamically without altering its structure.
  • This pattern is useful when you want to enhance or modify the behavior of objects at runtime without changing their code.

Example

// Base Interface
class ICourse {
    getDescription() {
        throw new Error("This method should be implemented by subclasses");
    }

    getCost() {
        throw new Error("This method should be implemented by subclasses");
    }
}

// Concrete Component
class BasicCourse extends ICourse {
    getDescription() {
        return "Basic Course";
    }

    getCost() {
        return 100.00;
    }
}

// Decorator
class CourseDecorator extends ICourse {
    constructor(course) {
        super();
        this.course = course;
    }

    getDescription() {
        return this.course.getDescription();
    }

    getCost() {
        return this.course.getCost();
    }
}

// Concrete Decorator 1
class PremiumContentDecorator extends CourseDecorator {
    constructor(course) {
        super(course);
    }

    getDescription() {
        return this.course.getDescription() + " with Premium Content";
    }

    getCost() {
        return this.course.getCost() + 50.00;
    }
}

// Main Function
function decoratorPatternExample() {
    let course = new BasicCourse();
    console.log("Description: " + course.getDescription());
    console.log("Cost: $" + course.getCost());

    let premiumCourse = new PremiumContentDecorator(course);
    console.log("\nDescription: " + premiumCourse.getDescription());
    console.log("Cost: $" + premiumCourse.getCost());
}

// Calling the main function
decoratorPatternExample();   

Output

Description: Basic Course
Cost: $100

Description: Basic Course with Premium Content
Cost: $150

Explanation

  • Interface and Implementation: ICourse interface outlines methods for course details, and BasicCourse provides their basic implementation.
  • Decorator Pattern: CourseDecorator enhances course functionality, with PremiumContentDecorator adding premium features and costs.
  • Usage: Shows how a basic course is wrapped with a premium decorator, modifying its description and cost

5. Facade Pattern Design Pattern

  • Facade Design Pattern provides a simplified interface to a complex system of classes, making it easier to interact with the system.
  • This pattern is like a front desk that handles communication with a complex backend, shielding users from the complexity.

Example

// Subsystem Classes
class CourseRegistration {
    registerCourse(courseName) {
        console.log("Registering for course: " + courseName);
    }
}

class PaymentProcessing {
    processPayment(amount) {
        console.log("Processing payment of $" + amount);
    }
}

class Certification {
    issueCertificate(courseName) {
        console.log("Issuing certificate for course: " + courseName);
    }
}

// Facade
class CourseFacade {
    constructor() {
        this.registration = new CourseRegistration();
        this.payment = new PaymentProcessing();
        this.certification = new Certification();
    }

    enrollInCourse(courseName, amount) {
        this.registration.registerCourse(courseName);
        this.payment.processPayment(amount);
        this.certification.issueCertificate(courseName);
    }
}

// Main Function
function facadePatternExample() {
    const facade = new CourseFacade();
    facade.enrollInCourse("JavaScript Programming", 100.00);
}

// Calling the main function
facadePatternExample();    

Output

  
Registering for course: JavaScript Programming
Processing payment of $100
Issuing certificate for course: JavaScript Programming

Explanation

  • Subsystem Classes: CourseRegistration, PaymentProcessing, and Certification handle specific functionalities related to course enrollment, payment, and certificate issuance.
  • Facade Class: CourseFacade simplifies interactions with these subsystems, providing a single method enrollInCourse to manage the entire enrollment process.
  • Usage: The facadePatternExample function demonstrates how to enroll in a course using the facade, which internally manages registration, payment processing, and certification.

6. Flyweight Design Pattern

  • The Flyweight Design Pattern minimizes memory usage by sharing as much data as possible between similar objects.
  • This pattern is useful when dealing with a large number of similar objects, as it reduces the overhead of object creation and memory consumption.

Example

// Flyweight Interface
class ICourse {
    display(studentName) {
        throw new Error("This method should be implemented by subclasses");
    }
}

// Concrete Flyweight
class ConcreteCourse extends ICourse {
    constructor(courseName) {
        super();
        this.courseName = courseName;
    }

    display(studentName) {
        console.log("Course: " + this.courseName + " for Student: " + studentName);
    }
}

// Flyweight Factory
class CourseFactory {
    constructor() {
        this.courseMap = new Map();
    }

    getCourse(courseName) {
        if (!this.courseMap.has(courseName)) {
            this.courseMap.set(courseName, new ConcreteCourse(courseName));
        }
        return this.courseMap.get(courseName);
    }
}

// Main Function
function flyweightPatternExample() {
    const factory = new CourseFactory();

    const javascriptCourse = factory.getCourse("JavaScript Programming");
    javascriptCourse.display("Aman");

    const pythonCourse = factory.getCourse("Python Programming");
    pythonCourse.display("Ankita");

    const javascriptCourse2 = factory.getCourse("JavaScript Programming");
    javascriptCourse2.display("Raghav");
}

// Calling the main function
flyweightPatternExample();   

Output

Course: JavaScript Programming for Student: Aman
Course: Python Programming for Student: Ankita
Course: JavaScript Programming for Student: Raghav

Explanation

  • Flyweight Pattern: Uses shared ConcreteCourse instances to minimize memory usage for similar courses.
  • Factory: CourseFactory manages course creation and reuse, storing existing courses in a map.
  • Example: Demonstrates course registration for multiple students while reusing the same course instances.

7. Proxy Pattern Design Pattern

  • Proxy Pattern provides a placeholder or surrogate for another object to control access to it.
  • This pattern is useful for managing access to resources, adding security, or optimizing performance by delaying expensive operations until they are needed.

Example

// Subject Interface
class IScholarHatCourse {
    enroll() {
        throw new Error("This method should be implemented by subclasses");
    }
}

// Real Subject
class RealCourse extends IScholarHatCourse {
    constructor(courseName) {
        super();
        this.courseName = courseName;
    }

    enroll() {
        console.log("Enrolling in course: " + this.courseName);
    }
}

// Proxy
class CourseProxy extends IScholarHatCourse {
    constructor(courseName) {
        super();
        this.courseName = courseName;
        this.realCourse = null;
    }

    enroll() {
        if (this.realCourse === null) {
            this.realCourse = new RealCourse(this.courseName);
        }
        this.realCourse.enroll();
    }
}

// Main Function
function proxyPatternExample() {
    const course = new CourseProxy("JavaScript Programming");
    course.enroll();  // RealCourse is created and enrolled in
}

// Calling the main function
proxyPatternExample();  

Output

Enrolling in course: JavaScript Programming  

Explanation

  • Proxy Pattern: CourseProxy controls access to RealCourse, creating it only when needed.
  • Real Subject: RealCourse implements the enrollment functionality for a specific course.
  • Example Usage: Demonstrates enrollment in a course via the proxy, which initializes the real subject on the first call.
3. Behavioral Design Patterns
  • Behavioral design patterns are a type of design pattern in developing software that deals with the communication and interaction of objects and classes.
  • They emphasize how objects and classes work together and communicate to complete tasks and responsibilities.

Behavioral Patterns

The following are types of behavioral patterns:

  1. Observer Pattern Design Pattern
  2. Strategy Pattern Design Pattern
  3. Command Pattern Design Pattern
  4. State Pattern Design Pattern
  5. Template Method Pattern Design Pattern
  6. Chain of Responsibility Pattern Design Pattern
  7. Mediator Pattern Design Pattern
  8. Memento Pattern Design Pattern
Let's explore one by one:

1. Observer Design Pattern

  • Observer Design Pattern allows an object to notify other objects about changes in its state.
  • This pattern is useful when you need a system where changes in one part of the system automatically update other parts, like a news feed where subscribers get updates when new articles are published.

Example

// Subject Interface
class ICourseSubject {
    addObserver(observer) {
        throw new Error("This method should be implemented by subclasses");
    }

    removeObserver(observer) {
        throw new Error("This method should be implemented by subclasses");
    }

    notifyObservers() {
        throw new Error("This method should be implemented by subclasses");
    }
}

// Concrete Subject
class Course extends ICourseSubject {
    constructor(courseName) {
        super();
        this.courseName = courseName;
        this.observers = [];
    }

    addObserver(observer) {
        this.observers.push(observer);
    }

    removeObserver(observer) {
        const index = this.observers.indexOf(observer);
        if (index > -1) {
            this.observers.splice(index, 1);
        }
    }

    notifyObservers() {
        for (const observer of this.observers) {
            observer.update(this.courseName);
        }
    }

    changeCourseName(newCourseName) {
        this.courseName = newCourseName;
        this.notifyObservers();
    }
}

// Observer Interface
class ICourseObserver {
    update(courseName) {
        throw new Error("This method should be implemented by subclasses");
    }
}

// Concrete Observer
class Student extends ICourseObserver {
    constructor(studentName) {
        super();
        this.studentName = studentName;
    }

    update(courseName) {
        console.log(`${this.studentName} received update: ${courseName}`);
    }
}

// Main Function
function observerPatternExample() {
    const course = new Course("JavaScript Programming");

    const student1 = new Student("Alice");
    const student2 = new Student("Bob");

    course.addObserver(student1);
    course.addObserver(student2);

    course.changeCourseName("Advanced JavaScript Programming");
}

// Calling the main function
observerPatternExample();   

Output

Alice received update: Advanced JavaScript Programming
Bob received update: Advanced JavaScript Programming

Explanation

  • Subject and Observers: Course manages a list of Student observers to notify them of updates.
  • Observer Implementation: Student implements the update method to receive course name changes.
  • Usage Example: Shows students being notified when the course name is updated.

2. Strategy Pattern Design Pattern

  • Strategy Design Pattern enables selecting an algorithm at runtime without altering the code that uses it.
  • This pattern is proper when you have multiple ways to operate, such as different sorting algorithms, and you want to choose the best one dynamically.
Read More:
Data Structures Sorting: Types and Examples Explained
Sort in Python- An Easy Way to Learn

Example

// Strategy Interface
class IPaymentStrategy {
    pay(amount) {
        throw new Error("This method should be implemented by subclasses");
    }
}

// Concrete Strategies
class CreditCardPayment extends IPaymentStrategy {
    pay(amount) {
        console.log(`Paid $${amount} using Credit Card.`);
    }
}

class PayPalPayment extends IPaymentStrategy {
    pay(amount) {
        console.log(`Paid $${amount} using PayPal.`);
    }
}

// Context
class ShoppingCart {
    setPaymentStrategy(paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

    checkout(amount) {
        this.paymentStrategy.pay(amount);
    }
}

// Main Function
function strategyPatternExample() {
    const cart = new ShoppingCart();

    cart.setPaymentStrategy(new CreditCardPayment());
    cart.checkout(100.00);

    cart.setPaymentStrategy(new PayPalPayment());
    cart.checkout(200.00);
}

// Calling the main function
strategyPatternExample();   

Output

Paid $100 using Credit Card.
Paid $200 using PayPal.

Explanation

  • Payment Strategies: Defines IPaymentStrategy interface with concrete strategies CreditCardPayment and PayPalPayment for different payment methods.
  • Shopping Cart Context: ShoppingCart class uses the payment strategy to process payments during checkout.
  • Example Usage: Demonstrates setting different payment strategies and checking out with varying amounts.

3. Command Design Pattern

  • Command Design Pattern turns requests or simple operations into objects, allowing them to be executed, undone, or queued.
  • This pattern is handy for implementing action history or undo mechanisms, like in a text editor where you can undo previous actions.

Example

// Command Interface
class ICommand {
    execute() {
        throw new Error("This method should be implemented by subclasses");
    }
}

// Concrete Command
class EnrollInCourseCommand extends ICommand {
    constructor(courseName) {
        super();
        this.courseName = courseName;
    }

    execute() {
        console.log(`Enrolled in course: ${this.courseName}`);
    }
}

// Invoker
class CourseRegistration {
    setCommand(command) {
        this.command = command;
    }

    performAction() {
        this.command.execute();
    }
}

// Main Function
function commandPatternExample() {
    const enrollCommand = new EnrollInCourseCommand("JavaScript Programming");
    const registration = new CourseRegistration();

    registration.setCommand(enrollCommand);
    registration.performAction();
}

// Calling the main function
commandPatternExample();  

Output

Enrolled in course: JavaScript Programming

Explanation

  • Command Pattern: Defines a command interface ICommand and a concrete command EnrollInCourseCommand to encapsulate the enrollment action.
  • Invoker Class: The CourseRegistration class acts as the invoker, holding a command and executing it when prompted.
  • Execution Example: Demonstrates usage by creating an enrollment command for "JavaScript Programming" and executing it through the registration class.

4. StateDesign Pattern

  • State Design Pattern allows an object to change its behavior when its internal state changes.
  • This pattern is useful for managing state-dependent behavior, such as a vending machine that changes its operations based on whether it's idle, accepting coins, or dispensing items.

Example

// State Interface
class ICourseState {
    handle() {
        throw new Error("This method should be implemented by subclasses");
    }
}

// Concrete States
class NotStartedState extends ICourseState {
    handle() {
        console.log("Course not started yet.");
    }
}

class InProgressState extends ICourseState {
    handle() {
        console.log("Course is in progress.");
    }
}

class CompletedState extends ICourseState {
    handle() {
        console.log("Course is completed.");
    }
}

// Context
class Course {
    setState(state) {
        this.state = state;
    }

    request() {
        this.state.handle();
    }
}

// Main Function
function statePatternExample() {
    const course = new Course();

    course.setState(new NotStartedState());
    course.request();

    course.setState(new InProgressState());
    course.request();

    course.setState(new CompletedState());
    course.request();
}

// Calling the main function
statePatternExample();   

Output

Course not started yet.
Course is in progress.
Course is completed.

Explanation

  • State Interface: ICourseState defines a method for handling different course statuses.
  • Concrete States: Implementations like NotStartedState, InProgressState, and CompletedState define specific behaviors for each course status.
  • Context: The Course class manages the current state and executes the corresponding behavior through the request method.

5. Template Method Design Pattern

  • Template Method Design Pattern defines the skeleton of an algorithm in a base class but lets subclasses override specific steps.
  • This pattern ensures a consistent structure while allowing customization, like a cooking recipe where the steps are fixed, but the ingredients can vary.

Example

// Abstract Class
class CourseTemplate {
    enroll() {
        this.selectCourse();
        this.makePayment();
        this.sendConfirmation();
    }

    selectCourse() {
        throw new Error("This method should be implemented by subclasses");
    }

    makePayment() {
        throw new Error("This method should be implemented by subclasses");
    }

    sendConfirmation() {
        console.log("Confirmation sent.");
    }
}

// Concrete Class 1
class JavaScriptCourse extends CourseTemplate {
    selectCourse() {
        console.log("JavaScript Programming course selected.");
    }

    makePayment() {
        console.log("Payment made for JavaScript course.");
    }
}

// Concrete Class 2
class PythonCourse extends CourseTemplate {
    selectCourse() {
        console.log("Python Programming course selected.");
    }

    makePayment() {
        console.log("Payment made for Python course.");
    }
}

// Main Class
class TemplateMethodPatternExample {
    static main() {
        const javaScriptCourse = new JavaScriptCourse();
        javaScriptCourse.enroll();

        const pythonCourse = new PythonCourse();
        pythonCourse.enroll();
    }
}

// Running the example
TemplateMethodPatternExample.main();  

Output

JavaScript Programming course selected.
Payment made for JavaScript course.
Confirmation sent.
Python Programming course selected.
Payment made for Python course.
Confirmation sent.  

Explanation

  • Template Method Pattern: CourseTemplate class defines the enrollment process via the enroll() method, outlining steps like selecting a course and making payment.
  • Concrete Classes: JavaScriptCourse and PythonCourse implement specific course selection and payment methods.
  • Execution: The TemplateMethodPatternExample class creates instances of the courses and calls enroll() to execute the enrollment process for each.

6. Chain of Responsibility Design Pattern

  • Chain of Responsibility Design Pattern passes a request along a chain of handlers until one of them handles it.
  • This pattern is useful for processing requests in a sequence, such as filtering and handling user inputs or processing various levels of a request in a web application.

Example

// Handler Interface
class CourseHandler {
    setNextHandler(handler) {
        this.nextHandler = handler;
    }

    handleRequest(request) {
        throw new Error("This method should be implemented by subclasses");
    }
}

// Concrete Handlers
class EnrollmentHandler extends CourseHandler {
    handleRequest(request) {
        if (request === "Enroll") {
            console.log("Handling enrollment.");
        } else if (this.nextHandler) {
            this.nextHandler.handleRequest(request);
        }
    }
}

class PaymentHandler extends CourseHandler {
    handleRequest(request) {
        if (request === "Payment") {
            console.log("Handling payment.");
        } else if (this.nextHandler) {
            this.nextHandler.handleRequest(request);
        }
    }
}

class CertificationHandler extends CourseHandler {
    handleRequest(request) {
        if (request === "Certificate") {
            console.log("Handling certification.");
        } else if (this.nextHandler) {
            this.nextHandler.handleRequest(request);
        }
    }
}

// Main Class
class ChainOfResponsibilityPatternExample {
    static main() {
        const enrollment = new EnrollmentHandler();
        const payment = new PaymentHandler();
        const certification = new CertificationHandler();

        enrollment.setNextHandler(payment);
        payment.setNextHandler(certification);

        enrollment.handleRequest("Payment");
        enrollment.handleRequest("Certificate");
    }
}

// Running the example
ChainOfResponsibilityPatternExample.main();   

Output

Handling payment.
Handling certification.

Explanation

  • Chain Setup: CourseHandler creates a chain with handlers like EnrollmentHandler, PaymentHandler, and CertificationHandler.
  • Request Handling: Each handler processes specific requests and forwards others to the next handler.
  • Demonstration: The example shows how requests for "Payment" and "Certificate" are handled through the chain.
7. Mediator Design Pattern
  • Mediator Design Pattern centralizes communication between objects, reducing the dependency between them.
  • This pattern is useful for managing complex interactions in a system, such as coordinating the components of a chat application to handle user messages and notifications.

Example

  
// Mediator Interface
class CourseMediator {
    registerStudent(student) {
        throw new Error("This method should be implemented by subclasses");
    }

    notifyStudents(message) {
        throw new Error("This method should be implemented by subclasses");
    }
}

// Concrete Mediator
class Course extends CourseMediator {
    constructor() {
        super();
        this.students = [];
    }

    registerStudent(student) {
        this.students.push(student);
    }

    notifyStudents(message) {
        for (const student of this.students) {
            student.update(message);
        }
    }
}

// Colleague
class Student {
    constructor(name, mediator) {
        this.name = name;
        this.mediator = mediator;
        mediator.registerStudent(this);
    }

    update(message) {
        console.log(`${this.name} received message: ${message}`);
    }
}

// Main Class
class MediatorPatternExample {
    static main() {
        const course = new Course();

        const student1 = new Student("Alice", course);
        const student2 = new Student("Bob", course);

        course.notifyStudents("Course registration opens tomorrow.");
    }
}

// Running the example
MediatorPatternExample.main();   

Output

Alice received message: Course registration opens tomorrow.
Bob received message: Course registration opens tomorrow.

Explanation

  • Mediator Setup: The Course class acts as a mediator that registers students and notifies them of messages.
  • Student Registration: Each Student instance registers with the mediator upon creation and can receive updates.
  • Notification: The example demonstrates how the mediator notifies all registered students with a message about course registration.

8. Memento Design Pattern

  • The Memento Design Pattern captures and restores an object's state without exposing its internal structure.
  • This pattern is helpful in saving and retrieving an object's state, like implementing an undo feature in a text editor where you can revert to a previous version of the document.

Example

  // Memento
class CourseMemento {
    constructor(courseName) {
        this.courseName = courseName;
    }

    getCourseName() {
        return this.courseName;
    }
}

// Originator
class Course {
    constructor() {
        this.courseName = '';
    }

    setCourseName(courseName) {
        this.courseName = courseName;
    }

    save() {
        return new CourseMemento(this.courseName);
    }

    restore(memento) {
        this.courseName = memento.getCourseName();
    }

    toString() {
        return `Course name: ${this.courseName}`;
    }
}

// Caretaker
class CourseCaretaker {
    constructor() {
        this.memento = null;
    }

    saveMemento(memento) {
        this.memento = memento;
    }

    getMemento() {
        return this.memento;
    }
}

// Main Class
class MementoPatternExample {
    static main() {
        const course = new Course();
        const caretaker = new CourseCaretaker();

        course.setCourseName("JavaScript Programming");
        console.log(course.toString());

        caretaker.saveMemento(course.save());

        course.setCourseName("Advanced JavaScript Programming");
        console.log(course.toString());

        course.restore(caretaker.getMemento());
        console.log(course.toString());
    }
}

// Running the example
MementoPatternExample.main();  

Output

Course name: JavaScript Programming
Course name: Advanced JavaScript Programming
Course name: JavaScript Programming

Explanation

  • Memento Class: CourseMemento holds the course name for state saving.
  • Originator Class: Course sets, saves, and restores its name using mementos.
  • Caretaker Class: CourseCaretaker manages the memento, enabling state restoration of the Course.

When to Use Design Patterns?

  • Reoccurring Problems: Use design patterns when you meet reoccurring design issues with well-defined answers. Design patterns provide tried-and-true approaches to typical program design difficulties.
  • Flexibility and Reusability: Use design patterns to encourage code reuse, flexibility, and maintenance. They assist in structuring code so that it is easier to adapt and extend as requirements change.
  • Design Principles: Use design patterns to implement core design principles including dividing up concerns, encapsulation, and dependency inversion. They aid in improving modularity and decreasing dependency between components.
  • Communication: Design patterns can help team members communicate more effectively. Design patterns provide a shared vocabulary and understanding of how to tackle certain challenges, which aids cooperation and code comprehension.
  • Performance: Design patterns can sometimes increase performance by minimizing resource utilization, lowering overhead, or increasing code execution efficiency.

How to Choose the Right Design Pattern?

Choosing a design pattern depends on the problem that you are solving and the requirements for solving that problem; consider the following:

  • Problem Type: Match the pattern to the nature of the problem, such as creational, structural, or behavioral issues.
  • Flexibility: Assess if the pattern allows flexibility and scalability during future changes.
  • Complexity: Assure the complexity of the pattern doesn't complexify your design but simplifies it.
  • Common Solutions: Observe well-used patterns in similar circumstances and leverage solutions proven to work.

Choosing the right pattern will increase the maintainability and effectiveness of your design.

Best Practices for Using Design Patterns

Best practices for using design patterns:

  • Understand the Purpose: Ensure you understand the pattern's purpose and its applicability in order to avoid misapplication.
  • Keep It Simple:Use patterns only if they really simplify a problem and, therefore, never introduce extraneous complexity.
  • Follow Conventions: Stick to existing naming conventions and patterns, as this will give the code much more readability.
  • Use of Document: Clearly document why and how the pattern is used so that it is useful to future developers.
Read more:
.Net Design Patterns Interview Questions, You Must Know!
Most Frequently Asked Software Architect Interview Questions and Answers
Summary

This tutorial presents an in-depth overview of design patterns in JavaScript, including definitions and implementations. It also includes creational, structural, and behavioral patterns. It begins with an overview of each category, followed by extensive explanations and sample code for patterns including Singleton, Factory Method, Adapter, and Observer. This tutorial shows when to utilize design patterns, how to select the proper one, and the best practices for successful implementation. Also, consider our Software Architecture and Design Certification Trainingfor a better understanding of other Java concepts.

FAQs

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It allows the algorithm to vary independently from clients that use it, making it easier to add new algorithms without modifying existing code.

Use unit testing frameworks (like Jest or Mocha) to write tests for the specific implementations of design patterns. Ensure that each component behaves as expected and that interactions between components function correctly.

Overusing design patterns can lead to unnecessarily complex code and may introduce performance overhead. It’s essential to apply them judiciously based on the project’s needs.

Online resources, repositories like GitHub, and coding blogs often provide examples and tutorials on implementing various design patterns in JavaScript.

Yes, design patterns can be applied in frameworks like React or Angular to enhance component structure, state management, and data flow, improving code organization and maintainability.

Share Article
About Author
Shailendra Chauhan (Microsoft MVP, Founder & CEO at Scholarhat by DotNetTricks)

Shailendra Chauhan, Founder and CEO of ScholarHat by DotNetTricks, is a renowned expert in System Design, Software Architecture, Azure Cloud, .NET, Angular, React, Node.js, Microservices, DevOps, and Cross-Platform Mobile App Development. His skill set extends into emerging fields like Data Science, Python, Azure AI/ML, and Generative AI, making him a well-rounded expert who bridges traditional development frameworks with cutting-edge advancements. Recognized as a Microsoft Most Valuable Professional (MVP) for an impressive 9 consecutive years (2016–2024), he has consistently demonstrated excellence in delivering impactful solutions and inspiring learners.

Shailendra’s unique, hands-on training programs and bestselling books have empowered thousands of professionals to excel in their careers and crack tough interviews. A visionary leader, he continues to revolutionize technology education with his innovative approach.
Accept cookies & close this