C++ Composite Pattern Complete Guide | Handling Tree Structures with Uniform Interface

C++ Composite Pattern Complete Guide | Handling Tree Structures with Uniform Interface

이 글의 핵심

C++ Composite Pattern: Handling tree structures with uniform interface. What is Composite pattern? Why needed, basic structure.

What is Composite Pattern? Why Needed

The flow of unifying tree and part-whole with one interface is good to read compared with other patterns in the Structural Pattern Series.

Problem Scenario: Processing Individual Objects and Groups Differently

Problem: Processing files and folders in different ways makes code complex. Below is an implementation example using C++. Process data with loops and perform branching with conditionals. Understand the role of each part while examining the code.

// Bad design: Type checking needed
void printSize(FileSystemItem* item) {
    if (auto* file = dynamic_cast<File*>(item)) {
        std::cout << file->getSize() << '\n';
    } else if (auto* folder = dynamic_cast<Folder*>(item)) {
        for (auto& child : folder->getChildren()) {
            printSize(child);  // Recursion
        }
    }
}

Solution: Composite pattern handles leaf (file) and composite (folder) with the same interface. Here is detailed implementation code using C++. Define a class to encapsulate data and functionality, and process data with loops. Understand the role of each part while examining the code.

// Good design: Composite
// Type definition
class Component {
public:
    virtual int getSize() const = 0;  // Unified interface
};
class File : public Component {
    int size_;
public:
    int getSize() const override { return size_; }
};
class Folder : public Component {
    std::vector<std::shared_ptr<Component>> children_;
public:
    int getSize() const override {
        int total = 0;
        for (const auto& child : children_)
            total += child->getSize();  // Recursion
        return total;
    }
};

Below is an implementation example using mermaid. Understand the role of each part while examining the code.

flowchart TD
    client[Client]
    component[Component
(getSize)] leaf[Leaf
(File)] composite[Composite
(Folder)] client --> component leaf -.implements.-> component composite -.implements.-> component composite --> component

Table of Contents

  1. Basic Structure
  2. File System Example
  3. UI Component Hierarchy
  4. Common Problems and Solutions
  5. Production Patterns
  6. Complete Example: Organization Chart System

1. Basic Structure

#include <vector>
#include <memory>
#include <iostream>
class Component {
public:
    virtual void operation() const = 0;
    virtual void add(std::shared_ptr<Component>) {}
    virtual void remove(std::shared_ptr<Component>) {}
    virtual ~Component() = default;
};
// Leaf: No children
class Leaf : public Component {
    int id_;
public:
    explicit Leaf(int id) : id_(id) {}
    void operation() const override {
        std::cout << "Leaf " << id_ << '\n';
    }
};
// Composite: Has children list
class Composite : public Component {
    std::vector<std::shared_ptr<Component>> children_;
public:
    void add(std::shared_ptr<Component> c) override {
        children_.push_back(std::move(c));
    }
    void remove(std::shared_ptr<Component> c) override {
        children_.erase(
            std::remove(children_.begin(), children_.end(), c),
            children_.end()
        );
    }
    void operation() const override {
        std::cout << "Composite [\n";
        for (const auto& c : children_)
            c->operation();
        std::cout << "]\n";
    }
};
int main() {
    auto root = std::make_shared<Composite>();
    root->add(std::make_shared<Leaf>(1));
    
    auto branch = std::make_shared<Composite>();
    branch->add(std::make_shared<Leaf>(2));
    branch->add(std::make_shared<Leaf>(3));
    root->add(branch);
    
    root->operation();
    // Output:
    // Composite [
    // Leaf 1
    // Composite [
    // Leaf 2
    // Leaf 3
    // ]
    // ]
    return 0;
}

2. File System Example

Calculating File and Folder Sizes

#include <vector>
#include <memory>
#include <iostream>
#include <string>
class FileSystemItem {
public:
    virtual int getSize() const = 0;
    virtual void print(int indent = 0) const = 0;
    virtual ~FileSystemItem() = default;
};
class File : public FileSystemItem {
    std::string name_;
    int size_;
public:
    File(std::string name, int size) : name_(std::move(name)), size_(size) {}
    
    int getSize() const override { return size_; }
    
    void print(int indent = 0) const override {
        std::cout << std::string(indent, ' ') << "File: " << name_ 
                  << " (" << size_ << " bytes)\n";
    }
};
class Folder : public FileSystemItem {
    std::string name_;
    std::vector<std::shared_ptr<FileSystemItem>> children_;
public:
    explicit Folder(std::string name) : name_(std::move(name)) {}
    
    void add(std::shared_ptr<FileSystemItem> item) {
        children_.push_back(std::move(item));
    }
    
    int getSize() const override {
        int total = 0;
        for (const auto& child : children_)
            total += child->getSize();
        return total;
    }
    
    void print(int indent = 0) const override {
        std::cout << std::string(indent, ' ') << "Folder: " << name_ 
                  << " (" << getSize() << " bytes total)\n";
        for (const auto& child : children_)
            child->print(indent + 2);
    }
};
int main() {
    auto root = std::make_shared<Folder>("root");
    root->add(std::make_shared<File>("readme.txt", 100));
    
    auto src = std::make_shared<Folder>("src");
    src->add(std::make_shared<File>("main.cpp", 500));
    src->add(std::make_shared<File>("utils.cpp", 300));
    root->add(src);
    
    auto docs = std::make_shared<Folder>("docs");
    docs->add(std::make_shared<File>("manual.pdf", 2000));
    root->add(docs);
    
    root->print();
    // Output:
    // Folder: root (2900 bytes total)
    //   File: readme.txt (100 bytes)
    //   Folder: src (800 bytes total)
    //     File: main.cpp (500 bytes)
    //     File: utils.cpp (300 bytes)
    //   Folder: docs (2000 bytes total)
    //     File: manual.pdf (2000 bytes)
    
    return 0;
}

Key Point: getSize() is called recursively to calculate the size of the entire tree.

Summary

Key Points

  1. Composite Pattern: Treats individual objects and compositions uniformly
  2. Component: Abstract base class defining common interface
  3. Leaf: Individual object with no children
  4. Composite: Container holding children
  5. Recursion: Operations propagate through tree structure

When to Use

Use Composite when:

  • Need to represent part-whole hierarchies
  • Want clients to treat individual and composite objects uniformly
  • Tree structures (file systems, UI components, organization charts) ❌ Don’t use when:
  • Flat structure with no hierarchy
  • Different operations for leaf and composite
  • Performance critical (recursion overhead)

Best Practices

  • ✅ Use smart pointers for memory management
  • ✅ Implement visitor pattern for complex operations
  • ✅ Consider caching for expensive calculations
  • ❌ Don’t expose internal structure
  • ❌ Don’t forget virtual destructors