Skip to main content

Command Palette

Search for a command to run...

Composite Design Pattern

Updated
β€’12 min read
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

  1. Component: Common interface for both leaf and composite objects

  2. Leaf: Represents individual objects with no children

  3. Composite: Contains children (leaves or other composites) and implements operations by delegating to children

  4. 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

  1. Tree Structures: You need to represent part-whole hierarchies

  2. Uniform Treatment: Clients should treat individual objects and compositions uniformly

  3. Recursive Structures: Operations need to work recursively on tree structures

  4. Nested Hierarchies: You have objects that can contain other objects of the same type

  5. Simplify Client Code: You want to eliminate type-checking and casting

❌ Avoid When

  1. No Hierarchy: Your objects don't form a natural tree structure

  2. Different Operations: Leaves and composites need fundamentally different operations

  3. Type Safety: You need strong compile-time type checking

  4. Simple Structures: The hierarchy is flat or very simple

Benefits

  1. Simplicity: Clients treat all objects uniformly

  2. Flexibility: Easy to add new component types

  3. Recursive Operations: Natural support for tree traversal

  4. Open/Closed Principle: Add new components without changing existing code

  5. Single Responsibility: Each component handles its own behavior

Drawbacks

  1. Overly General: Can make design too general

  2. Type Safety: Harder to restrict component types

  3. Complexity: Can be overkill for simple hierarchies

  4. 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

  1. Use Common Interface: Ensure all components implement the same interface

  2. Handle Edge Cases: Check for null children and empty composites

  3. Consider Caching: Cache computed values (like size) for performance

  4. Document Tree Operations: Clearly explain how operations traverse the tree

  5. Provide Iterator: Consider providing an iterator for tree traversal

  6. Thread Safety: Make composites thread-safe if needed

  7. 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! 🌳✨