Composite Design Pattern

The Composite Pattern is a structural design pattern that lets you compose objects into tree structures to represent part-whole hierarchies. It allows clients to treat individual objects and compositions of objects uniformlyβlike treating a single file and a folder full of files the same way.
Think of a file system: a folder can contain files or other folders. You can perform operations like "delete" or "copy" on both a single file and an entire folder, and the system handles them uniformly. That's the Composite Pattern in action!
The Problem
Imagine you're building a graphics editor where you need to work with simple shapes (circles, rectangles) and complex drawings (groups of shapes). Without the Composite Pattern, you'd need different code to handle individual shapes versus groups:
// Without Composite Pattern - Messy!
if (object instanceof Circle) {
Circle circle = (Circle) object;
circle.draw();
} else if (object instanceof Rectangle) {
Rectangle rect = (Rectangle) object;
rect.draw();
} else if (object instanceof Group) {
Group group = (Group) object;
for (Shape shape : group.getShapes()) {
// Recursively handle each shape... π΅
}
}
This approach is error-prone, hard to maintain, and violates the Open/Closed Principle. Every time you add a new shape type, you need to update all the conditional logic.
The Solution: Composite Pattern
The Composite Pattern creates a tree structure where both leaf nodes (individual objects) and composite nodes (containers) implement the same interface, allowing uniform treatment.
Key Components
Component: Common interface for both leaf and composite objects
Leaf: Represents individual objects with no children
Composite: Contains children (leaves or other composites) and implements operations by delegating to children
Client: Works with objects through the Component interface
Tree Structure Visualization
Real-World Implementation
Example 1: File System
import java.util.ArrayList;
import java.util.List;
// Component Interface
public interface FileSystemComponent {
void display(String indent);
long getSize();
String getName();
}
// Leaf - File
public class File implements FileSystemComponent {
private String name;
private long size;
public File(String name, long size) {
this.name = name;
this.size = size;
}
@Override
public void display(String indent) {
System.out.println(indent + "π " + name + " (" + size + " bytes)");
}
@Override
public long getSize() {
return size;
}
@Override
public String getName() {
return name;
}
}
// Composite - Directory
public class Directory implements FileSystemComponent {
private String name;
private List<FileSystemComponent> children;
public Directory(String name) {
this.name = name;
this.children = new ArrayList<>();
}
public void add(FileSystemComponent component) {
children.add(component);
}
public void remove(FileSystemComponent component) {
children.remove(component);
}
@Override
public void display(String indent) {
System.out.println(indent + "π " + name + "/");
for (FileSystemComponent child : children) {
child.display(indent + " ");
}
}
@Override
public long getSize() {
long totalSize = 0;
for (FileSystemComponent child : children) {
totalSize += child.getSize();
}
return totalSize;
}
@Override
public String getName() {
return name;
}
}
// Client Code
public class FileSystemDemo {
public static void main(String[] args) {
// Create files
File file1 = new File("document.txt", 1024);
File file2 = new File("image.jpg", 2048);
File file3 = new File("video.mp4", 10240);
File file4 = new File("readme.md", 512);
// Create directories
Directory root = new Directory("root");
Directory documents = new Directory("documents");
Directory media = new Directory("media");
// Build tree structure
root.add(file4);
root.add(documents);
root.add(media);
documents.add(file1);
media.add(file2);
media.add(file3);
// Display structure
root.display("");
// Calculate total size
System.out.println("\nTotal size: " + root.getSize() + " bytes");
}
}
/* Output:
π root/
π readme.md (512 bytes)
π documents/
π document.txt (1024 bytes)
π media/
π image.jpg (2048 bytes)
π video.mp4 (10240 bytes)
Total size: 13824 bytes
*/
Example 2: Organization Hierarchy
import java.util.ArrayList;
import java.util.List;
// Component Interface
public interface Employee {
void showDetails(String indent);
double getSalary();
String getName();
}
// Leaf - Individual Employee
public class Developer implements Employee {
private String name;
private String position;
private double salary;
public Developer(String name, String position, double salary) {
this.name = name;
this.position = position;
this.salary = salary;
}
@Override
public void showDetails(String indent) {
System.out.println(indent + "π¨βπ» " + name + " - " + position +
" ($" + salary + ")");
}
@Override
public double getSalary() {
return salary;
}
@Override
public String getName() {
return name;
}
}
// Composite - Manager with Team
public class Manager implements Employee {
private String name;
private String position;
private double salary;
private List<Employee> team;
public Manager(String name, String position, double salary) {
this.name = name;
this.position = position;
this.salary = salary;
this.team = new ArrayList<>();
}
public void addTeamMember(Employee employee) {
team.add(employee);
}
public void removeTeamMember(Employee employee) {
team.remove(employee);
}
@Override
public void showDetails(String indent) {
System.out.println(indent + "π " + name + " - " + position +
" ($" + salary + ") [Team of " + team.size() + "]");
for (Employee member : team) {
member.showDetails(indent + " ");
}
}
@Override
public double getSalary() {
double totalSalary = salary;
for (Employee member : team) {
totalSalary += member.getSalary();
}
return totalSalary;
}
@Override
public String getName() {
return name;
}
}
// Client Code
public class OrganizationDemo {
public static void main(String[] args) {
// Create developers
Developer dev1 = new Developer("Alice", "Senior Developer", 90000);
Developer dev2 = new Developer("Bob", "Junior Developer", 60000);
Developer dev3 = new Developer("Charlie", "Developer", 75000);
Developer dev4 = new Developer("Diana", "Developer", 75000);
// Create managers
Manager teamLead = new Manager("Eve", "Team Lead", 100000);
Manager cto = new Manager("Frank", "CTO", 150000);
// Build organization structure
teamLead.addTeamMember(dev1);
teamLead.addTeamMember(dev2);
cto.addTeamMember(teamLead);
cto.addTeamMember(dev3);
cto.addTeamMember(dev4);
// Display organization
System.out.println("Organization Structure:");
cto.showDetails("");
// Calculate total payroll
System.out.println("\nTotal Payroll: $" + cto.getSalary());
}
}
/* Output:
Organization Structure:
π Frank - CTO ($150000.0) [Team of 3]
π Eve - Team Lead ($100000.0) [Team of 2]
π¨βπ» Alice - Senior Developer ($90000.0)
π¨βπ» Bob - Junior Developer ($60000.0)
π¨βπ» Charlie - Developer ($75000.0)
π¨βπ» Diana - Developer ($75000.0)
Total Payroll: $550000.0
*/
Example 3: Graphics System
import java.util.ArrayList;
import java.util.List;
// Component Interface
public interface Graphic {
void draw();
void move(int x, int y);
}
// Leaf - Circle
public class Circle implements Graphic {
private int x, y, radius;
public Circle(int x, int y, int radius) {
this.x = x;
this.y = y;
this.radius = radius;
}
@Override
public void draw() {
System.out.println("Drawing Circle at (" + x + "," + y +
") with radius " + radius);
}
@Override
public void move(int deltaX, int deltaY) {
x += deltaX;
y += deltaY;
System.out.println("Circle moved to (" + x + "," + y + ")");
}
}
// Leaf - Rectangle
public class Rectangle implements Graphic {
private int x, y, width, height;
public Rectangle(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
@Override
public void draw() {
System.out.println("Drawing Rectangle at (" + x + "," + y +
") with size " + width + "x" + height);
}
@Override
public void move(int deltaX, int deltaY) {
x += deltaX;
y += deltaY;
System.out.println("Rectangle moved to (" + x + "," + y + ")");
}
}
// Composite - Group of Graphics
public class GraphicGroup implements Graphic {
private String name;
private List<Graphic> graphics;
public GraphicGroup(String name) {
this.name = name;
this.graphics = new ArrayList<>();
}
public void add(Graphic graphic) {
graphics.add(graphic);
}
public void remove(Graphic graphic) {
graphics.remove(graphic);
}
@Override
public void draw() {
System.out.println("Drawing Group: " + name);
for (Graphic graphic : graphics) {
graphic.draw();
}
}
@Override
public void move(int deltaX, int deltaY) {
System.out.println("Moving Group: " + name);
for (Graphic graphic : graphics) {
graphic.move(deltaX, deltaY);
}
}
}
// Client Code
public class GraphicsDemo {
public static void main(String[] args) {
// Create individual shapes
Circle circle1 = new Circle(10, 10, 5);
Circle circle2 = new Circle(20, 20, 8);
Rectangle rect1 = new Rectangle(30, 30, 15, 10);
Rectangle rect2 = new Rectangle(50, 50, 20, 15);
// Create groups
GraphicGroup group1 = new GraphicGroup("Circles");
group1.add(circle1);
group1.add(circle2);
GraphicGroup group2 = new GraphicGroup("Rectangles");
group2.add(rect1);
group2.add(rect2);
GraphicGroup mainGroup = new GraphicGroup("All Shapes");
mainGroup.add(group1);
mainGroup.add(group2);
// Draw all
mainGroup.draw();
System.out.println("\n--- Moving all shapes ---\n");
// Move all at once
mainGroup.move(5, 5);
}
}
Workflow Diagram
Real-World Use Cases
When to Use the Composite Pattern
β Use When
Tree Structures: You need to represent part-whole hierarchies
Uniform Treatment: Clients should treat individual objects and compositions uniformly
Recursive Structures: Operations need to work recursively on tree structures
Nested Hierarchies: You have objects that can contain other objects of the same type
Simplify Client Code: You want to eliminate type-checking and casting
β Avoid When
No Hierarchy: Your objects don't form a natural tree structure
Different Operations: Leaves and composites need fundamentally different operations
Type Safety: You need strong compile-time type checking
Simple Structures: The hierarchy is flat or very simple
Benefits
Simplicity: Clients treat all objects uniformly
Flexibility: Easy to add new component types
Recursive Operations: Natural support for tree traversal
Open/Closed Principle: Add new components without changing existing code
Single Responsibility: Each component handles its own behavior
Drawbacks
Overly General: Can make design too general
Type Safety: Harder to restrict component types
Complexity: Can be overkill for simple hierarchies
Performance: Recursive operations can be expensive for deep trees
Advanced Example: Menu System
import java.util.ArrayList;
import java.util.List;
// Component Interface
public interface MenuComponent {
void display(String indent);
void execute();
String getName();
}
// Leaf - Menu Item
public class MenuItem implements MenuComponent {
private String name;
private String action;
public MenuItem(String name, String action) {
this.name = name;
this.action = action;
}
@Override
public void display(String indent) {
System.out.println(indent + "β " + name);
}
@Override
public void execute() {
System.out.println("Executing: " + action);
}
@Override
public String getName() {
return name;
}
}
// Composite - Menu (can contain items or submenus)
public class Menu implements MenuComponent {
private String name;
private List<MenuComponent> items;
public Menu(String name) {
this.name = name;
this.items = new ArrayList<>();
}
public void add(MenuComponent component) {
items.add(component);
}
public void remove(MenuComponent component) {
items.remove(component);
}
@Override
public void display(String indent) {
System.out.println(indent + "βΌ " + name);
for (MenuComponent item : items) {
item.display(indent + " ");
}
}
@Override
public void execute() {
System.out.println("Opening menu: " + name);
for (MenuComponent item : items) {
item.execute();
}
}
@Override
public String getName() {
return name;
}
}
// Client Code
public class MenuDemo {
public static void main(String[] args) {
// Create menu items
MenuItem newFile = new MenuItem("New", "Create new file");
MenuItem open = new MenuItem("Open", "Open file");
MenuItem save = new MenuItem("Save", "Save file");
MenuItem saveAs = new MenuItem("Save As", "Save file as");
MenuItem exit = new MenuItem("Exit", "Exit application");
MenuItem cut = new MenuItem("Cut", "Cut selection");
MenuItem copy = new MenuItem("Copy", "Copy selection");
MenuItem paste = new MenuItem("Paste", "Paste from clipboard");
// Create submenus
Menu fileMenu = new Menu("File");
fileMenu.add(newFile);
fileMenu.add(open);
fileMenu.add(save);
fileMenu.add(saveAs);
fileMenu.add(exit);
Menu editMenu = new Menu("Edit");
editMenu.add(cut);
editMenu.add(copy);
editMenu.add(paste);
// Create main menu
Menu mainMenu = new Menu("Main Menu");
mainMenu.add(fileMenu);
mainMenu.add(editMenu);
// Display menu structure
mainMenu.display("");
System.out.println("\n--- Executing File Menu ---\n");
fileMenu.execute();
}
}
Pattern Comparison
| Pattern | Purpose | Structure | Use Case |
|---|---|---|---|
| Composite | Treat objects uniformly in trees | Tree hierarchy | File systems, UI components |
| Decorator | Add responsibilities dynamically | Wrapping chain | Add features to objects |
| Chain of Resp | Pass request along chain | Linear chain | Event handling, logging |
| Flyweight | Share common state efficiently | Shared objects | Large numbers of similar objects |
Composite vs Decorator
Best Practices
Use Common Interface: Ensure all components implement the same interface
Handle Edge Cases: Check for null children and empty composites
Consider Caching: Cache computed values (like size) for performance
Document Tree Operations: Clearly explain how operations traverse the tree
Provide Iterator: Consider providing an iterator for tree traversal
Thread Safety: Make composites thread-safe if needed
Avoid Circular References: Prevent children from referencing ancestors
Safety Considerations
// Safe Composite Implementation
public class SafeComposite implements Component {
private List<Component> children = new ArrayList<>();
private Component parent;
public void add(Component component) {
// Prevent circular references
if (component == this || isAncestor(component)) {
throw new IllegalArgumentException("Cannot add ancestor as child");
}
children.add(component);
if (component instanceof SafeComposite) {
((SafeComposite) component).parent = this;
}
}
private boolean isAncestor(Component component) {
Component current = this.parent;
while (current != null) {
if (current == component) {
return true;
}
if (current instanceof SafeComposite) {
current = ((SafeComposite) current).parent;
} else {
break;
}
}
return false;
}
@Override
public void operation() {
for (Component child : children) {
child.operation();
}
}
}
Conclusion
The Composite Pattern is your go-to solution for working with tree structures where you want to treat individual objects and compositions uniformly. Whether you're building file systems, UI frameworks, or organizational hierarchies, the Composite Pattern provides an elegant way to handle part-whole relationships.
Remember: Think trees, not types! π³
π― Key Takeaway
The Composite Pattern is about treating the one and the many the same way. When you have tree structures, let the pattern handle the complexity!
Why did the developer use the Composite Pattern for their family tree? Because they wanted to treat their relatives recursivelyβwhether it's one annoying cousin or an entire branch of them! π¨βπ©βπ§βπ¦π
Happy Composing! π³β¨




