Community for developers to learn, share their programming knowledge. Register!
Design Patterns in JavaScript

Structural Design Patterns in JavaScript


Welcome to this article on Structural Design Patterns using JavaScript! If you're looking to enhance your knowledge and skills in software design, you're in the right place. This article will provide you with a comprehensive overview of structural design patterns, illustrating how to apply them effectively in JavaScript.

What are Structural Design Patterns?

Structural Design Patterns are design patterns that deal with object composition, helping to form larger structures while maintaining flexibility and efficiency. They enable developers to create relationships between objects in a way that makes it easier to manage and scale the codebase. By using these patterns, you can simplify complex systems, enhance code reusability, and ensure that your application remains manageable.

In JavaScript, structural design patterns leverage the language's prototypal inheritance and dynamic object capabilities. They help in building scalable applications by promoting code organization and reducing dependencies among components. Let's explore some commonly used structural design patterns in JavaScript.

Adapter Pattern: Bridging Interfaces

The Adapter Pattern acts as a bridge between two incompatible interfaces. It allows classes to work together that couldn’t otherwise due to incompatible interfaces. This pattern is particularly useful when you want to integrate new functionalities into existing systems without altering their structure.

Example

Consider a scenario where you have a legacy system that expects an object with a specific method signature. You can create an adapter to transform the new object into the expected format.

class LegacySystem {
    request() {
        return "Data from the legacy system";
    }
}

class NewSystem {
    fetch() {
        return "Data from the new system";
    }
}

class Adapter {
    constructor(newSystem) {
        this.newSystem = newSystem;
    }
    
    request() {
        return this.newSystem.fetch();
    }
}

// Usage
const legacySystem = new LegacySystem();
const newSystem = new NewSystem();
const adapter = new Adapter(newSystem);

console.log(legacySystem.request()); // Outputs: Data from the legacy system
console.log(adapter.request()); // Outputs: Data from the new system

In this example, the Adapter class allows the NewSystem to be used wherever the LegacySystem is expected, thus maintaining compatibility without modifying the existing code.

Decorator Pattern: Adding Functionality Dynamically

The Decorator Pattern allows behavior to be added to individual objects dynamically without affecting the behavior of other objects from the same class. This pattern is beneficial for adhering to the Open-Closed Principle, which states that software entities should be open for extension but closed for modification.

Example

Let’s create a coffee shop example where we can dynamically add various toppings to a coffee order.

class Coffee {
    cost() {
        return 5; // base cost of coffee
    }
}

class MilkDecorator {
    constructor(coffee) {
        this.coffee = coffee;
    }
    
    cost() {
        return this.coffee.cost() + 1; // Adds cost of milk
    }
}

class SugarDecorator {
    constructor(coffee) {
        this.coffee = coffee;
    }
    
    cost() {
        return this.coffee.cost() + 0.5; // Adds cost of sugar
    }
}

// Usage
let myCoffee = new Coffee();
console.log(myCoffee.cost()); // Outputs: 5

myCoffee = new MilkDecorator(myCoffee);
console.log(myCoffee.cost()); // Outputs: 6

myCoffee = new SugarDecorator(myCoffee);
console.log(myCoffee.cost()); // Outputs: 6.5

In this example, you can see how the Decorator pattern allows us to enhance the functionality of the Coffee class without modifying its structure.

Facade Pattern: Simplifying Complex Interfaces

The Facade Pattern provides a simplified interface to a complex subsystem. It acts as a front-facing interface masking the complex underlying logic. This pattern is particularly useful when integrating multiple components or libraries, offering a cohesive interface to the user.

Example

Consider a scenario where you have a complex system for handling user authentication, sending emails, and logging activities. Instead of exposing all these classes and their methods, you can create a Facade that simplifies interactions.

class UserAuth {
    login(user) {
        // Logic for user login
        return `${user} logged in!`;
    }
}

class EmailService {
    sendEmail(user) {
        // Logic for sending email
        return `Email sent to ${user}`;
    }
}

class Logger {
    log(message) {
        // Logic for logging
        console.log(`Log: ${message}`);
    }
}

class UserFacade {
    constructor() {
        this.auth = new UserAuth();
        this.emailService = new EmailService();
        this.logger = new Logger();
    }
    
    registerUser(user) {
        const loginMessage = this.auth.login(user);
        const emailMessage = this.emailService.sendEmail(user);
        this.logger.log(`${loginMessage}, ${emailMessage}`);
    }
}

// Usage
const userFacade = new UserFacade();
userFacade.registerUser("John Doe");

By using the UserFacade, clients can interact with the system without needing to understand its complexity.

Composite Pattern: Treating Individual and Composite Objects Uniformly

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

Example

Imagine a file system where files and directories can be treated uniformly. You can create a composite structure that allows for nested directories and files.

class File {
    constructor(name) {
        this.name = name;
    }

    display() {
        console.log(`File: ${this.name}`);
    }
}

class Directory {
    constructor(name) {
        this.name = name;
        this.children = [];
    }

    add(child) {
        this.children.push(child);
    }

    display() {
        console.log(`Directory: ${this.name}`);
        this.children.forEach(child => child.display());
    }
}

// Usage
const root = new Directory("root");
const file1 = new File("file1.txt");
const file2 = new File("file2.txt");
const subDir = new Directory("subDir");

root.add(file1);
root.add(subDir);
subDir.add(file2);

root.display();

This example demonstrates how the Composite pattern allows directories to contain both files and other directories, enabling a flexible and manageable structure.

Proxy Pattern: Controlling Access to Objects

The Proxy Pattern provides a surrogate or placeholder for another object to control access to it. This pattern is useful for lazy initialization, access control, logging, or monitoring actions.

Example

Consider a situation where you want to control access to a resource-intensive object.

class RealImage {
    constructor(filename) {
        this.filename = filename;
        this.loadImageFromDisk();
    }

    loadImageFromDisk() {
        console.log(`Loading ${this.filename}`);
    }

    display() {
        console.log(`Displaying ${this.filename}`);
    }
}

class ProxyImage {
    constructor(filename) {
        this.filename = filename;
        this.realImage = null;
    }

    display() {
        if (!this.realImage) {
            this.realImage = new RealImage(this.filename);
        }
        this.realImage.display();
    }
}

// Usage
const image = new ProxyImage("image.jpg");
image.display(); // Loads and displays the image
image.display(); // Displays the image without loading again

In this example, the ProxyImage class controls access to the RealImage class, ensuring that the resource is only loaded when necessary.

Summary

In this article, we've explored various Structural Design Patterns using JavaScript, including the Adapter, Decorator, Facade, Composite, and Proxy patterns. Each pattern serves a unique purpose in software design, helping to manage complexity, enhance reusability, and promote cleaner architecture.

By applying these patterns, you can build more scalable and maintainable JavaScript applications. Understanding and utilizing structural design patterns is crucial for intermediate and professional developers looking to refine their craft and improve their software design skills.

Last Update: 18 Jan, 2025

Topics:
JavaScript