Thursday, 12 December 2024

Composite Design Pattern | JavaScript Design Patterns

The Composite Design Pattern is a structural design pattern that allows you to treat individual objects and compositions of objects uniformly. It is particularly useful for representing part-whole hierarchies, where individual objects and collections of objects are treated in a similar way. This pattern is commonly used when you need to build a tree-like structure.

Key Concepts:

  1. Component: This is the common interface for both leaf objects and composite objects. It defines the operations that can be performed on both individual objects and groups of objects.

  2. Leaf: A leaf is an individual object in the structure. It implements the Component interface and doesn't have any children.

  3. Composite: A composite object is a collection of Component objects (which could be either leaf or other composite objects). It implements the Component interface and can have child components.

Example of Composite Pattern in JavaScript

Let's consider a scenario where we have a file system, with files (leaf) and folders (composite). Both files and folders can have the getSize method, but a folder can contain other folders or files.

Without the Composite Pattern (Non-Uniform Handling):

class File {

    constructor(name, size) {

        this.name = name;

        this.size = size;

    }

 

    getSize() {

        return this.size;

    }

}

 

class Folder {

    constructor(name) {

        this.name = name;

        this.children = [];

    }

 

    add(child) {

        this.children.push(child);

    }

 

    getSize() {

        let totalSize = 0;

        for (let child of this.children) {

            totalSize += child.getSize();

        }

        return totalSize;

    }

}

 

const file1 = new File("file1.txt", 10);

const file2 = new File("file2.txt", 20);

 

const folder1 = new Folder("folder1");

folder1.add(file1);

folder1.add(file2);

 

console.log(`Folder size: ${folder1.getSize()}`); // 30

In this example, the Folder class has to manage its children manually by iterating over them and summing up their sizes. If we wanted to add other kinds of elements, we'd need to modify this logic. This becomes cumbersome as the complexity grows.

With the Composite Pattern (Uniform Handling):

Now, let's refactor the code using the Composite Pattern, where both File and Folder share the same interface and are treated uniformly.

// Component: Common interface

class FileSystemComponent {

    constructor(name) {

        this.name = name;

    }

 

    getSize() {

        throw new Error("This method should be overridden!");

    }

}

 

// Leaf: File object

class File extends FileSystemComponent {

    constructor(name, size) {

        super(name);

        this.size = size;

    }

 

    getSize() {

        return this.size;

    }

}

 

// Composite: Folder object that can contain other components

class Folder extends FileSystemComponent {

    constructor(name) {

        super(name);

        this.children = [];

    }

 

    add(child) {

        this.children.push(child);

    }

 

    getSize() {

        let totalSize = 0;

        for (let child of this.children) {

            totalSize += child.getSize();

        }

        return totalSize;

    }

}

 

// Usage:

const file1 = new File("file1.txt", 10);

const file2 = new File("file2.txt", 20);

 

const folder1 = new Folder("folder1");

folder1.add(file1);

folder1.add(file2);

 

const file3 = new File("file3.txt", 15);

const folder2 = new Folder("folder2");

folder2.add(file3);

folder2.add(folder1); // folder2 contains folder1

 

console.log(`File3 size: ${file3.getSize()}`); // 15

console.log(`Folder1 size: ${folder1.getSize()}`); // 30

console.log(`Folder2 size: ${folder2.getSize()}`); // 45 (15 + 30)

Explanation:

  1. FileSystemComponent: This is the Component interface, providing the getSize method, which is overridden by both File and Folder.

  2. File: The Leaf class. It has a name and a size. The getSize method returns the size of the file.

  3. Folder: The Composite class. It has a collection of children (which could be File or other Folder objects). It overrides the getSize method to calculate the total size by recursively calling getSize on its children.

Advantages of the Composite Pattern:

  1. Uniformity: Both leaf and composite objects implement the same interface, making the handling of individual objects and compositions transparent and uniform.

  2. Flexibility: You can add new types of components (like different types of files or folders) without modifying existing code. New components can simply be added to the hierarchy.

  3. Ease of Use: The composite object (e.g., Folder) can be treated the same as the individual leaf objects (e.g., File) in client code, simplifying operations like calculating total size, printing contents, etc.

  4. Recursive Structure: The pattern naturally supports recursive structures, where composites can contain other composites.

When to Use the Composite Pattern:

  • When you need to represent part-whole hierarchies (like a filesystem, organization chart, or product catalog).

  • When you want to treat individual objects and groups of objects uniformly.

  • When you need to add new types of components to a structure without changing existing code.

 

No comments:

Post a Comment