C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
142: The Composite Pattern
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
FileSystemItemwith a virtual methodgetSize()that returns the size of the item in bytes. - A leaf class
Filethat stores a name and a fixed size. - A composite class
Directorythat stores a name and a list ofFileSystemItempointers. ItsgetSize()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.
There are no comments for now.