Skip to content

Composite Pattern

Intent

The Composite pattern composes objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.

Problem

When working with tree structures that represent part-whole hierarchies, you often need to:

  • Treat both individual objects (leaves) and compositions of objects (composites) uniformly
  • Perform operations on individual objects and groups of objects in the same way
  • Build complex tree structures from simpler components

For example, in a graphics application, you might have primitive shapes (circles, rectangles) and composite shapes (groups of shapes). You want to be able to draw, move, or resize both individual shapes and groups of shapes without distinguishing between them.

Without the Composite pattern, client code becomes cluttered with type-checking logic to determine whether it's dealing with an individual object or a collection.

Solution

The Composite pattern suggests defining a common interface (Component) for both simple objects (Leaf) and containers (Composite). This allows clients to treat individual objects and compositions uniformly through the Component interface.

The key is that both Leaf and Composite classes implement the same interface, and Composite can contain both Leaf and other Composite objects, forming a recursive tree structure.

Structure

Composite Pattern Diagram 0

Participants

  • Component - Declares the interface for objects in the composition - Implements default behavior common to all classes - Declares interface for accessing and managing child components - In our example: Component abstract class
  • Leaf - Represents leaf objects in the composition (no children) - Defines behavior for primitive objects - In our example: Leaf class
  • Composite - Defines behavior for components having children - Stores child components - Implements child-related operations in the Component interface - In our example: Composite class
  • Client - Manipulates objects in the composition through the Component interface - Works uniformly with both leaf and composite objects

Implementation

Component Interface

class Component {
protected:
    Component* parent_;

public:
    Component() : parent_(nullptr) {}
    virtual ~Component() = default;

    void setParent(Component* parent) {
        this->parent_ = parent;
    }

    Component* getParent() const {
        return this->parent_;
    }

    virtual void add(std::shared_ptr<Component> component) {
        (void)component;
    }

    virtual void remove(std::shared_ptr<Component> component) {
        (void)component;
    }

    virtual bool isComposite() const {
        return false;
    }

    virtual std::string operation() const = 0;
};

Leaf Class

1
2
3
4
5
6
class Leaf : public Component {
public:
    std::string operation() const override {
        return "Leaf";
    }
};

Composite Class

class Composite : public Component {
private:
    std::vector<std::shared_ptr<Component>> children_;

public:
    void add(std::shared_ptr<Component> component) override {
        this->children_.push_back(component);
        component->setParent(this);
    }

    void remove(std::shared_ptr<Component> component) override {
        auto it = std::find(children_.begin(), children_.end(), component);
        if (it != children_.end()) {
            (*it)->setParent(nullptr);
            children_.erase(it);
        }
    }

    bool isComposite() const override {
        return true;
    }

    std::string operation() const override {
        std::string result = "Branch(";
        for (size_t i = 0; i < children_.size(); ++i) {
            if (i > 0) {
                result += "+";
            }
            result += children_[i]->operation();
        }
        result += ")";
        return result;
    }
};

Usage Example

// Client code works with components uniformly
void clientCode(const Component* component) {
    std::cout << "RESULT: " << component->operation() << std::endl;
}

int main() {
    // Simple leaf
    auto simple = std::make_shared<Leaf>();
    clientCode(simple.get());

    // Complex composite tree
    auto tree = std::make_shared<Composite>();

    auto branch1 = std::make_shared<Composite>();
    branch1->add(std::make_shared<Leaf>());
    branch1->add(std::make_shared<Leaf>());

    tree->add(branch1);
    tree->add(std::make_shared<Leaf>());

    // Client treats both uniformly
    clientCode(tree.get());

    return 0;
}

Applicability

Use the Composite pattern when:

  • You want to represent part-whole hierarchies of objects - Tree structures where nodes can be either leaves or containers - Example: File system, organization chart, UI component hierarchy
  • You want clients to ignore the difference between compositions and individual objects - Clients can treat all objects uniformly through the Component interface - Simplifies client code - no need for type checking
  • You need to implement tree structures - Recursive composition of objects - Operations should traverse the entire tree
  • You want to add new kinds of components easily - New leaf or composite types can be added without changing existing code - Follows Open/Closed Principle

Consequences

Benefits

  1. Uniform treatment of objects - Clients can treat individual objects and compositions uniformly - Simplifies client code - no type checking needed

  2. Simplified client code - Client code doesn't need to know if it's dealing with a leaf or composite - Single interface for all operations

  3. Easy to add new components - New leaf or composite types can be added easily - Existing code doesn't need to change

  4. Flexible structure - Easy to build complex trees from simple components - Can create arbitrarily deep hierarchies

Drawbacks

  1. Overly general design - Component interface must support both leaf and composite operations - May include methods that don't make sense for all component types

  2. Difficult to restrict component types - Hard to ensure a composite contains only certain types of components - Type system doesn't enforce such restrictions naturally

  3. Potential for misuse - Calling composite operations (add/remove) on leaf nodes - Need to handle these cases gracefully

Design Considerations

1. Explicit Parent References

Including parent references allows: - Easy traversal up the tree - Implementation of Chain of Responsibility - Managing component lifecycle

1
2
3
void setParent(Component* parent) {
    this->parent_ = parent;
}

2. Sharing Components

Decide whether components can have multiple parents: - Single parent: Simpler, tree structure - Multiple parents: More flexible, graph structure (requires careful memory management)

3. Child Management

Where should add/remove operations be defined?

Option 1: In Component (used in our example) - Pros: Uniform interface, transparency - Cons: Leaf nodes have meaningless operations

Option 2: Only in Composite - Pros: Type safety, no meaningless operations on leaves - Cons: Loses transparency, clients must check type

4. Caching

Composites can cache results of operations: - Cache aggregated results (e.g., total size) - Invalidate cache when structure changes - Trade-off between speed and memory

5. Ordering Children

Decide if child order matters: - Use vector if order matters - Use set or unordered_set if order doesn't matter - Consider performance implications

Real-world Examples

  1. File Systems - Leaf: File - Composite: Directory - Operations: getSize(), delete(), search() - Both files and directories implement the same interface

  2. GUI Components - Leaf: Button, Label, TextBox - Composite: Panel, Container, Window - Operations: render(), handleEvent(), layout() - Containers can contain both widgets and other containers

  3. Organization Structures - Leaf: Employee - Composite: Department - Operations: getSalary(), print() - Departments contain employees and sub-departments

  4. Graphics Editors - Leaf: Primitive shapes (Circle, Rectangle, Line) - Composite: Group - Operations: draw(), move(), resize() - Groups can contain shapes and other groups

  5. Document Structure - Leaf: Character, Image - Composite: Paragraph, Section, Document - Operations: render(), spellCheck() - Hierarchical document structure

  • Decorator: Often used together. Decorator adds responsibilities while maintaining the same interface. Composite groups components into tree structures.
  • Flyweight: Can be used to share leaf components when there are many identical instances.
  • Iterator: Can be used to traverse composite structures.
  • Visitor: Can be used to apply operations over composite structures without modifying the component classes.
  • Chain of Responsibility: Often used with Composite. Parent references in components allow requests to be passed up the tree.

Implementation Variations

1. Safety vs Transparency

Safety Approach: Child management only in Composite

class Component {
    virtual std::string operation() const = 0;
    // No add/remove methods
};

class Composite : public Component {
    void add(Component* c);
    void remove(Component* c);
    // Only Composite has child management
};

Transparency Approach: Child management in Component (our example) - Clients can treat all components uniformly - Leaf nodes have meaningless add/remove operations

2. Component Storage

Different ways to store children:

1
2
3
4
5
6
7
8
// Vector (maintains order)
std::vector<std::shared_ptr<Component>> children_;

// Set (unique elements, no specific order)
std::set<std::shared_ptr<Component>> children_;

// Map (keyed access)
std::map<std::string, std::shared_ptr<Component>> children_;

Sample Output

=== Composite Pattern Demo ===

Client: I've got a simple component:
RESULT: Leaf

Client: Now I've got a composite tree:
RESULT: Branch(Branch(Leaf+Leaf)+Branch(Leaf))

Client: I don't need to check the component class:
RESULT: Branch(Branch(Leaf+Leaf)+Branch(Leaf)+Leaf)

Client: Building a more complex tree:
RESULT: Branch(Branch(Leaf+Leaf+Branch(Leaf))+Branch(Leaf)+Leaf)

=== Real-world Scenario ===

Scenario: File System Structure
- Leaf: File (cannot contain other elements)
- Composite: Directory (can contain files and subdirectories)
- Both implement Component interface (e.g., getSize(), delete())

File System Tree Structure:
root/
├── documents/
│   ├── file1.txt (Leaf)
│   ├── file2.txt (Leaf)
│   └── projects/ (Composite)
│       └── project.doc (Leaf)
├── images/
│   └── photo.jpg (Leaf)
└── readme.txt (Leaf)

Operations:
- getSize() on a file: returns file size
- getSize() on a directory: returns sum of all contained files
- delete() on a file: deletes the file
- delete() on a directory: deletes directory and all contents
- Client treats files and directories uniformly!

Key Takeaways

  1. Composite pattern represents part-whole hierarchies as tree structures
  2. Clients treat individual objects and compositions uniformly
  3. Both Leaf and Composite implement the same Component interface
  4. Composites can contain both leaves and other composites (recursive structure)
  5. Simplifies client code by eliminating type-checking logic
  6. Trade-off between safety (type-safe operations) and transparency (uniform interface)
  7. Common in file systems, GUI frameworks, and document structures
  8. Works well with Iterator, Visitor, and Decorator patterns