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:
- Client needs different handling of
File
andDirectory
. - 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
Leave a comment
Your email address will not be published. Required fields are marked *