Skip to Content
Course content

142: The Composite Pattern

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've probably run into this situation before: you're building a system where things can be either a single "item" or a "collection of items," and you want to treat both exactly the same way. A classic example is a graphics editor. You have circles, squares, and lines. But then you realize you need a "Group" feature, where a user can select five shapes and treat them as one single entity—which can then be grouped again with other groups.

The "Special Case" Trap

When I first encountered this, my instinct was to keep the "Leaf" objects (the circles and squares) separate from the "Container" objects (the groups). I'd create a Shape base class for the primitives and a Group class that held a std::vector<Shape*>. At first glance, it seems logical. Why would a group be a shape? A group is a manager of shapes.

The problem hits you the moment you try to nest groups. If Group doesn't inherit from Shape, you can't put a Group inside another Group. You end up writing redundant code: one function to draw a shape, another to draw a group, and then a third, increasingly complex function to draw a "Group of Groups." You start using if (type == GROUP) checks everywhere, and your codebase begins to smell like a series of hacks.

// The naive way - this breaks when you want nested groups
class Shape {
public:
    virtual void draw() = 0;
};

class Circle : public Shape {
    void draw() override { /* Draw circle */ }
};

class Group {
    std::vector<Shape*> children;
public:
    void add(Shape* s) { children.push_back(s); }
    void drawAll() {
        for (auto s : children) s->draw();
    }
};

Unifying the Hierarchy

The Composite Pattern solves this by insisting that the container is a version of the thing it contains. We create a common interface—let's call it Graphic—that both the individual shapes and the groups implement. Now, the Group doesn't hold a list of Shape objects; it holds a list of Graphic objects. Since the Group itself is a Graphic, it can be added to another Group without the parent ever knowing the difference.

I like this approach because it moves the complexity out of the client code and into the structure of the objects. The "client" (the part of your app calling draw()) doesn't need to care if it's dealing with a single pixel or a complex hierarchy of a thousand nested groups. It just calls draw() and lets the recursion handle the rest.

class Graphic {
public:
    virtual ~Graphic() = default;
    virtual void draw() = 0;
};

class Circle : public Graphic {
public:
    void draw() override { std::cout < "Drawing Circle\n"; }
};

class Group : public Graphic {
    std::vector<std::unique_ptr<Graphic>> children;
public:
    void add(std::unique_ptr<Graphic> g) {
        children.push_back(std::move(g));
    }
    
    void draw() override {
        std::cout < "Drawing Group start\n";
        for (auto& child : children) {
            child->draw(); // Recursive call
        }
        std::cout < "Drawing Group end\n";
    }
};

The Trade-off: Generality vs. Specificity

Now, this isn't a free lunch. By unifying the interface, you're making a deal: you gain recursive power, but you lose some type-specific certainty. If your Graphic interface is too lean, you might find yourself wanting to call a method that only exists in Group (like add()) while holding a Graphic* pointer. You can't do that without a dynamic_cast, which is exactly the kind of thing we try to avoid in clean C++.

You have two choices here. You can either put the add() and remove() methods in the base Graphic class (and make them do nothing or throw an exception for Circle), or you can accept that the client must know when it's dealing with a Group to modify the structure. Personally, I prefer keeping the modification methods in the Composite class. It's better to have a slight inconvenience when building the tree than to pollute the base interface with methods that don't make sense for 70% of your subclasses.




📋 Practical Task

Implementation: Recursive File System Directory Tree

Build a simplified file system representation using the Composite Pattern. You need to implement the following:

  • A base class FileSystemItem with a virtual method getSize() that returns the size of the item in bytes.
  • A leaf class File that stores a name and a fixed size.
  • A composite class Directory that stores a name and a list of FileSystemItem pointers. Its getSize() method must recursively calculate the sum of the sizes of all files and sub-directories it contains.

Requirement: Your main function should create a directory structure at least three levels deep (e.g., Root -> Documents -> Work -> report.pdf) and call getSize() on the root directory to verify the total size is calculated correctly.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.