Updated on 03 Oct, 202529 mins read 304 views

The Composite Pattern helps when you have objects that can be organized into a tree structure.

It allows you to treat individual objects (leaves) and groups of objects (composites) uniformly.

Definition

The Composite Pattern is a structural design pattern that lets you treat individual objects and compositions of objects (groups) uniformly or same way.

Problem (Without Composite)

Suppose you are designing a file system:

  • A File is a leaf.
  • A Directory can contain multiple files and sub-directories.

If you don't use Composite, the client must handle files and directories differently:

#include <iostream>
#include <vector>
using namespace std;

class File {
    string name;
public:
    File(string n) : name(n) {}
    void showName() { cout << "File: " << name << "\n"; }
};

class Directory {
    string name;
    vector<File*> files;
public:
    Directory(string n) : name(n) {}
    void addFile(File* f) { files.push_back(f); }
    void showContent() {
        cout << "Directory: " << name << "\n";
        for (auto f : files) {
            f->showName();
        }
    }
};

int main() {
    File f1("resume.pdf");
    File f2("photo.jpg");
    Directory dir("Documents");

    dir.addFile(&f1);
    dir.addFile(&f2);

    // ❌ Client must handle File and Directory differently
    f1.showName();
    dir.showContent();
}

Problems:

  1. Client needs different handling of File and Directory.
  2. If tomorrow we add nested directories, client becomes messy.

Solution (With Composite)

We define a common interface (FileSystemComponent) for both File and Directory, so the client can treat them uniformly.

#include <iostream>
#include <vector>
using namespace std;

// Common Component
class FileSystemComponent {
public:
    virtual void showDetails(int indent = 0) = 0;
    virtual ~FileSystemComponent() = default;
};

// Leaf: File
class File : public FileSystemComponent {
    string name;
public:
    File(string n) : name(n) {}
    void showDetails(int indent = 0) override {
        cout << string(indent, ' ') << "File: " << name << "\n";
    }
};

// Composite: Directory
class Directory : public FileSystemComponent {
    string name;
    vector<FileSystemComponent*> children;
public:
    Directory(string n) : name(n) {}
    void add(FileSystemComponent* comp) { children.push_back(comp); }

    void showDetails(int indent = 0) override {
        cout << string(indent, ' ') << "Directory: " << name << "\n";
        for (auto child : children) {
            child->showDetails(indent + 2);  // recursive
        }
    }
};

// Client
int main() {
    File* f1 = new File("resume.pdf");
    File* f2 = new File("photo.jpg");

    Directory* docs = new Directory("Documents");
    docs->add(f1);
    docs->add(f2);

    Directory* root = new Directory("Root");
    root->add(docs);
    root->add(new File("system.log"));

    // ✅ Client treats everything the same
    root->showDetails();

    // Cleanup (in real-world, use smart pointers)
    delete f1;
    delete f2;
    delete docs;
    delete root;
}

Output:

Directory: Root
  Directory: Documents
    File: resume.pdf
    File: photo.jpg
  File: system.log

Understanding Leaf and Composite in the Composite Pattern

In this pattern we categorize components into two main roles:

1 Leaf (the basic unit | Individual Object):

  • A Leaf is a single, indivisible object in the hierarchy.
  • It does not contain other objects.
  • It only performs its own operation.
  • Example in real life:
    • A single Employee without subordinates.
    • A File in a filesystem.
    • A Button in a GUI.

2 Composite (the container / group)

  • A Composite is a container that can hold leaves or other composites.
  • It implements the same interface as leaves, so clients don't know if they are dealing with a leaf or a composite.
  • A composite usually delegates work to its children.
  • Example in real life:
    • A Manager who has employees under them.
    • A Folder (Directory) that contains files and other folders.
    • A Panel in a GUI that holds buttons, text fields, and other panels.

Key Idea:

  • Leaf = an atomic object (cannot have children).
    • These are the end nodes in the tree (no children).
  • Composite =  a container that groups other components (can be leaves or composites).
    • These are the branches (they hold leaves or more branches) in tree.

Code Example:

#include <iostream>
#include <vector>
#include <string>
#include <stdexcept>

using namespace std;

// Base interface for files and folders
class FileSystemItem {
public:
    virtual ~FileSystemItem() {}
    virtual void ls(int indent = 0) = 0;            
    virtual void openAll(int indent = 0) = 0;      
    virtual int getSize() = 0;                  
    virtual FileSystemItem* cd(const string& name) = 0; 
    virtual string getName() = 0;
    virtual bool isFolder() = 0;
};

// Leaf: File
class File : public FileSystemItem {
    string name;
    int size;
public:
    File(const string& n, int s) {
        name = n;
        size = s;
    }

    void ls(int indent = 0) override {
        cout << string(indent, ' ') << name << "\n";
    }

    void openAll(int indent = 0) override {
        cout << string(indent, ' ') << name << "\n";
    }

    int getSize() override {
        return size;
    }

    FileSystemItem* cd(const string&) override {
        return nullptr;
    }

    string getName() override {
        return name;
    }

    bool isFolder() override {
        return false;
    }
};

class Folder : public FileSystemItem {
    string name;
    vector<FileSystemItem*> children;
public:
    Folder(const string& n) {
        name = n;
    }
    ~Folder() {
        for (auto c : children) delete c;
    }

    void add(FileSystemItem* item) {
        children.push_back(item);
    }

    void ls(int indent = 0) override {
        for (auto child : children) {
            if (child->isFolder()) {
                cout <<string(indent, ' ') << "+ " << child->getName() << "\n";
            } else {
                cout <<string(indent, ' ') <<child->getName() << "\n";
            }
        }
    }

    void openAll(int indent = 0) override {
        cout << string(indent, ' ') << "+ " << name << "\n";
        for (auto child : children) {
            child->openAll(indent + 4);
        }
    }

    int getSize() override {
        int total = 0;
        for (auto child : children) {
            total += child->getSize();
        }
        return total;
    }

    FileSystemItem* cd(const string& target) override {
        for (auto child : children) {
            if (child->isFolder() && child->getName() == target) {
                return child;
            }
        }
        // not found or not a folder
        return nullptr;
    }

    string getName() override {
        return name;
    }
    bool isFolder() override {
        return true;
    }
};

int main() {
    // Build file system
    Folder* root = new Folder("root");
    root->add(new File("file1.txt", 1));
    root->add(new File("file2.txt", 1));

    Folder* docs = new Folder("docs");
    docs->add(new File("resume.pdf", 1));
    docs->add(new File("notes.txt", 1));
    root->add(docs);

    Folder* images = new Folder("images");
    images->add(new File("photo.jpg", 1));
    root->add(images);

    root->ls();

    docs->ls();

    root->openAll();

    FileSystemItem* cwd = root->cd("docs");
    if (cwd != nullptr) {
        cwd->ls();
    } else {
        cout << "\n Could not cd into docs \n";
    }

     cout << root->getSize();

    // Cleanup
    delete root;
    return 0;
}

Output:

file1.txt
file2.txt
+ docs
+ images
resume.pdf
notes.txt
+ root
    file1.txt
    file2.txt
    + docs
        resume.pdf
        notes.txt
    + images
        photo.jpg
resume.pdf
notes.txt
5 
Buy Me A Coffee

Leave a comment

Your email address will not be published. Required fields are marked *