In-Depth Explanation of the Composite Pattern in C++ Design Patterns

The Composite Pattern is a structural design pattern that allows you to compose objects into tree structures to represent part-whole hierarchies. This pattern enables clients to treat individual objects and compositions of objects uniformly, without needing to differentiate between handling a single object or an entire object tree.

Core Roles of the Composite Pattern

  1. Abstract Component (Component): Defines a common interface for leaf nodes and composite nodes.
  2. Leaf Node (Leaf): Represents leaf objects in the tree structure, which do not have child nodes.
  3. Composite Node (Composite): Represents composite objects that contain child nodes, which can be either leaf nodes or other composite nodes.

Example Implementation of the Composite Pattern

Below is an example of the Composite Pattern implemented using a “file system”. The file system consists of files (leaf nodes) and folders (composite nodes), where folders can contain files or other folders:

#include <iostream>
#include <string>
#include <vector>
#include <memory>

// Abstract Component: File System Node
class FileSystemComponent {
protected:
    std::string name;

public:
    FileSystemComponent(const std::string& componentName) : name(componentName) {}
    virtual ~FileSystemComponent() = default;

    // Pure virtual function: Display node information
    virtual void display(int depth = 0) const = 0;

    // Virtual function: Add child node (default implementation does not exist, leaf nodes will ignore)
    virtual void add(std::shared_ptr<FileSystemComponent> component) {
        // Empty implementation, leaf nodes do not support add operation
    }

    // Virtual function: Remove child node (default implementation does not exist)
    virtual void remove(const std::string& componentName) {
        // Empty implementation, leaf nodes do not support remove operation
    }
};

// Leaf Node: File
class File : public FileSystemComponent {
private:
    int size;  // File size (KB)

public:
    File(const std::string& fileName, int fileSize)
        : FileSystemComponent(fileName), size(fileSize) {}

    // Display file information
    void display(int depth = 0) const override {
        std::string indent(depth, '-');
        std::cout << indent << "File: " << name << " (" << size << "KB)" << std::endl;
    }

    // File does not support adding child nodes, overridden to empty implementation
    void add(std::shared_ptr<FileSystemComponent> component) override {}

    // File does not support removing child nodes
    void remove(const std::string& componentName) override {}
};

// Composite Node: Folder
class Folder : public FileSystemComponent {
private:
    // Store child nodes (can be files or other folders)
    std::vector<std::shared_ptr<FileSystemComponent>> children;

public:
    Folder(const std::string& folderName) : FileSystemComponent(folderName) {}

    // Display folder information and all child nodes
    void display(int depth = 0) const override {
        std::string indent(depth, '-');
        std::cout << indent << "Folder: " << name << std::endl;

        // Recursively display all child nodes, depth + 1
        for (const auto& child : children) {
            child->display(depth + 2);
        }
    }

    // Add child node
    void add(std::shared_ptr<FileSystemComponent> component) override {
        children.push_back(component);
    }

    // Remove child node
    void remove(const std::string& componentName) override {
        for (auto it = children.begin(); it != children.end(); ++it) {
            if ((*it)->name == componentName) {
                children.erase(it);
                std::cout << "Removed: " << componentName << std::endl;
                return;
            }
        }
        std::cout << "Not found: " << componentName << std::endl;
    }
};

// Client usage
int main() {
    // Create files
    auto readme = std::make_shared<File>("readme.txt", 10);
    auto image = std::make_shared<File>("photo.jpg", 2048);
    auto data = std::make_shared<File>("data.csv", 512);

    // Create subfolder
    auto docsFolder = std::make_shared<Folder>("Documents");
    docsFolder->add(std::make_shared<File>("report.docx", 1024));
    docsFolder->add(std::make_shared<File>("resume.pdf", 2048));

    // Create main folder and add content
    auto mainFolder = std::make_shared<Folder>("My Files");
    mainFolder->add(readme);
    mainFolder->add(image);
    mainFolder->add(docsFolder);

    // Display the entire file system structure
    std::cout << "=== Initial File System Structure ===" << std::endl;
    mainFolder->display();

    // Remove a file and add a new file
    mainFolder->remove("photo.jpg");
    mainFolder->add(data);

    // Display updated structure
    std::cout << "\n=== Updated File System Structure ===" << std::endl;
    mainFolder->display();

    return 0;
}

How the Composite Pattern Works

  1. The abstract component defines a common interface for leaf nodes and composite nodes, ensuring that clients can handle them uniformly.
  2. Leaf nodes are the lowest level of the tree structure and do not contain child nodes, implementing basic functionality.
  3. Composite nodes contain a collection of child nodes, can add and remove child nodes, and recursively handle operations on child nodes.
  4. Clients operate on the entire tree structure through the abstract component interface, without needing to differentiate between handling a single object or a composite object.

Two Implementation Approaches of the Composite Pattern

  • Transparent Mode: The abstract component declares all methods for managing child nodes (as shown in the example), and leaf nodes implement these methods as empty.
  • Safe Mode: The abstract component only declares basic operations, and methods for managing child nodes are declared in composite nodes, requiring clients to distinguish between leaf and composite nodes.

The transparent mode aligns more closely with the original intent of the composite pattern, allowing clients to uniformly handle all components.

Application Scenarios of the Composite Pattern

  1. Scenarios that represent part-whole hierarchical structures (e.g., file systems, organizational structures).
  2. Scenarios where clients need to uniformly handle single objects and composite objects.
  3. Scenarios for maintaining and displaying tree structures (e.g., UI component trees, XML/JSON document structures).
  4. Scenarios where recursive processing of all nodes in a tree structure is desired.

Advantages and Disadvantages of the Composite Pattern

Advantages:

  • Clients can uniformly handle single objects and composite objects, simplifying client code.
  • Facilitates the extension of new component types, adhering to the open-closed principle.
  • Allows for convenient recursive processing of the entire tree structure.
  • Clearly represents the hierarchical structure of objects.

Disadvantages:

  • Design can be complex, requiring a reasonable abstraction of the component interface.
  • May limit the characteristics of leaf nodes, as they need to implement the composite node’s interface.
  • For complex tree structures, performance issues may arise.

The Composite Pattern is widely used in GUI libraries, such as the QWidget system in Qt, where each window component can be a simple control (leaf) or a container that includes other controls (composite), allowing clients to uniformly handle all components.

Leave a Comment