Composite Pattern - Treating Individual Objects and Compositions Uniformly

Learning Objectives: By the end of this lesson, you will be able to:

  • Understand the Composite pattern and when to use it for tree structures
  • Implement composite and leaf objects with uniform interfaces
  • Apply Composite pattern to file systems, UI components, and organizational hierarchies
  • Handle recursive operations and traversal strategies effectively
  • Design flexible composite structures that support dynamic composition

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

Think about a company’s organizational chart. You have individual employees (leaves) and departments that contain other employees or sub-departments (composites). Whether you’re calculating total salary costs or sending company-wide announcements, you want to treat both individual employees and entire departments the same way. How would you design a system that handles this uniformly?

Lesson Overview

  • Duration: 120 minutes
  • Prerequisites: Object-oriented programming, tree data structures, recursive thinking
  • Materials Needed: Development environment with Java, Python, or JavaScript support

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. Composite Structure - Understanding leaf and composite objects with uniform interfaces
  2. Tree Operations - Implementing add, remove, and traverse operations
  3. Recursive Patterns - Handling operations that work on both individual and grouped objects
  4. Real-world Applications - File systems, UI frameworks, document structures
  5. Design Considerations - Safety vs transparency trade-offs, child management strategies
  6. Performance Optimization - Efficient traversal and caching in composite structures

Success Criteria:

By the end of this session, you should be able to design and implement composite structures for hierarchical data.

3. Foundation Knowledge (15-20 minutes)

Core Concepts

What is the Composite Pattern?

The Composite pattern allows you to compose objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions of objects uniformly through a common interface.

Real-World Analogy: Think of a file system where both files and folders implement the same interface. You can perform operations like “get size” or “delete” on both individual files and entire folders. The folder operation recursively applies to all its contents.

Why This Matters: When you have hierarchical data where you need to perform the same operations on both individual items and groups of items, Composite pattern provides a elegant solution that eliminates the need for type checking.

Key Components

  • Component: Common interface for both simple and complex objects
  • Leaf: Represents end objects (no children) that implement Component interface
  • Composite: Has children and implements Component interface by delegating to children
  • Client: Works with objects through the Component interface

Key Terminology

  • Uniform Interface: Same operations available on both leaf and composite objects
  • Recursive Composition: Composites can contain other composites, creating tree structures
  • Transparent Composite: Child management operations in Component interface
  • Safe Composite: Child management operations only in Composite class
  • Part-Whole Hierarchy: Tree structure where composites represent “wholes” and leaves represent “parts”

4. Detailed Explanation and Examples (25-30 minutes)

Understanding Composite Pattern Structure

The Composite pattern is essential when:

  • You have hierarchical data that forms tree structures
  • You want to treat individual objects and groups uniformly
  • You need to perform operations recursively on tree structures
  • You want clients to ignore the difference between compositions and individual objects
  • You have part-whole relationships that need uniform treatment

Design Trade-offs

Transparency vs Safety:

  • Transparent: Add/remove methods in Component interface - clients can treat all objects uniformly but may call inappropriate methods
  • Safe: Add/remove methods only in Composite - type-safe but clients must know object types

Practical Applications

Composite pattern is commonly used for:

  • File Systems: Files and directories with uniform operations (copy, delete, get size)
  • UI Frameworks: Containers and components (Swing, HTML DOM, React components)
  • Document Processing: Text, paragraphs, sections, chapters with formatting operations
  • Graphics Systems: Shapes, groups, drawing commands with render operations
  • Menu Systems: Menu items and sub-menus with display and navigation
  • Mathematical Expressions: Numbers, operators, sub-expressions with evaluation

Common Misconceptions

  1. Misconception: Composite pattern is only for GUI components Reality: Useful for any hierarchical structure where uniform treatment is needed

  2. Misconception: All operations must be implemented recursively Reality: Some operations might only apply to composites or leaves specifically

5. Live Coding Demonstration (20-25 minutes)

Example 1: File System Implementation

// Component interface
interface FileSystemComponent {
    String getName();
    long getSize();
    void display(String indent);
    FileSystemComponent findByName(String name);
    List<FileSystemComponent> findByExtension(String extension);
    void accept(FileSystemVisitor visitor);
}

// Leaf implementation
class File implements FileSystemComponent {
    private String name;
    private long size;
    private String content;
    private Date lastModified;
    
    public File(String name, long size) {
        this.name = name;
        this.size = size;
        this.content = "";
        this.lastModified = new Date();
    }
    
    public File(String name, String content) {
        this.name = name;
        this.content = content;
        this.size = content.length();
        this.lastModified = new Date();
    }
    
    @Override
    public String getName() {
        return name;
    }
    
    @Override
    public long getSize() {
        return size;
    }
    
    @Override
    public void display(String indent) {
        System.out.println(indent + "šŸ“„ " + name + " (" + size + " bytes)");
    }
    
    @Override
    public FileSystemComponent findByName(String name) {
        return this.name.equals(name) ? this : null;
    }
    
    @Override
    public List<FileSystemComponent> findByExtension(String extension) {
        List<FileSystemComponent> result = new ArrayList<>();
        if (name.endsWith("." + extension)) {
            result.add(this);
        }
        return result;
    }
    
    @Override
    public void accept(FileSystemVisitor visitor) {
        visitor.visitFile(this);
    }
    
    public String getContent() {
        return content;
    }
    
    public Date getLastModified() {
        return lastModified;
    }
    
    public String getExtension() {
        int lastDot = name.lastIndexOf('.');
        return lastDot > 0 ? name.substring(lastDot + 1) : "";
    }
}

// Composite implementation
class Directory implements FileSystemComponent {
    private String name;
    private List<FileSystemComponent> children;
    private Date created;
    
    public Directory(String name) {
        this.name = name;
        this.children = new ArrayList<>();
        this.created = new Date();
    }
    
    @Override
    public String getName() {
        return name;
    }
    
    @Override
    public long getSize() {
        long totalSize = 0;
        for (FileSystemComponent child : children) {
            totalSize += child.getSize();
        }
        return totalSize;
    }
    
    @Override
    public void display(String indent) {
        System.out.println(indent + "šŸ“ " + name + "/ (" + children.size() + " items, " + getSize() + " bytes)");
        for (FileSystemComponent child : children) {
            child.display(indent + "  ");
        }
    }
    
    @Override
    public FileSystemComponent findByName(String name) {
        if (this.name.equals(name)) {
            return this;
        }
        
        for (FileSystemComponent child : children) {
            FileSystemComponent found = child.findByName(name);
            if (found != null) {
                return found;
            }
        }
        return null;
    }
    
    @Override
    public List<FileSystemComponent> findByExtension(String extension) {
        List<FileSystemComponent> result = new ArrayList<>();
        for (FileSystemComponent child : children) {
            result.addAll(child.findByExtension(extension));
        }
        return result;
    }
    
    @Override
    public void accept(FileSystemVisitor visitor) {
        visitor.visitDirectory(this);
        for (FileSystemComponent child : children) {
            child.accept(visitor);
        }
    }
    
    // Composite-specific operations
    public void add(FileSystemComponent component) {
        children.add(component);
    }
    
    public void remove(FileSystemComponent component) {
        children.remove(component);
    }
    
    public List<FileSystemComponent> getChildren() {
        return new ArrayList<>(children);
    }
    
    public int getChildCount() {
        return children.size();
    }
    
    public Date getCreated() {
        return created;
    }
}

// Visitor pattern for complex operations
interface FileSystemVisitor {
    void visitFile(File file);
    void visitDirectory(Directory directory);
}

// Example visitors
class SizeCalculatorVisitor implements FileSystemVisitor {
    private long totalSize = 0;
    private int fileCount = 0;
    private int directoryCount = 0;
    
    @Override
    public void visitFile(File file) {
        totalSize += file.getSize();
        fileCount++;
    }
    
    @Override
    public void visitDirectory(Directory directory) {
        directoryCount++;
    }
    
    public long getTotalSize() { return totalSize; }
    public int getFileCount() { return fileCount; }
    public int getDirectoryCount() { return directoryCount; }
}

class BackupVisitor implements FileSystemVisitor {
    private List<String> backupPaths = new ArrayList<>();
    private String targetDirectory;
    
    public BackupVisitor(String targetDirectory) {
        this.targetDirectory = targetDirectory;
    }
    
    @Override
    public void visitFile(File file) {
        String backupPath = targetDirectory + "/" + file.getName();
        backupPaths.add(backupPath);
        System.out.println("Backing up file: " + file.getName() + " to " + backupPath);
    }
    
    @Override
    public void visitDirectory(Directory directory) {
        String backupPath = targetDirectory + "/" + directory.getName();
        System.out.println("Creating backup directory: " + backupPath);
    }
    
    public List<String> getBackupPaths() {
        return new ArrayList<>(backupPaths);
    }
}

Example 2: UI Component System

// Component interface for UI elements
interface UIComponent {
    void render();
    void handleEvent(UIEvent event);
    Dimension getSize();
    void setSize(Dimension size);
    boolean isVisible();
    void setVisible(boolean visible);
    UIComponent findById(String id);
    List<UIComponent> findByType(Class<? extends UIComponent> type);
}

// Leaf components
class Button implements UIComponent {
    private String id;
    private String text;
    private Dimension size;
    private boolean visible = true;
    private ActionListener actionListener;
    
    public Button(String id, String text) {
        this.id = id;
        this.text = text;
        this.size = new Dimension(100, 30);
    }
    
    @Override
    public void render() {
        if (visible) {
            System.out.println("Rendering Button: " + text + " [" + size.width + "x" + size.height + "]");
        }
    }
    
    @Override
    public void handleEvent(UIEvent event) {
        if (event.getType() == UIEvent.Type.CLICK && event.getTarget().equals(id)) {
            System.out.println("Button clicked: " + text);
            if (actionListener != null) {
                actionListener.actionPerformed();
            }
        }
    }
    
    @Override
    public Dimension getSize() { return new Dimension(size); }
    
    @Override
    public void setSize(Dimension size) { this.size = new Dimension(size); }
    
    @Override
    public boolean isVisible() { return visible; }
    
    @Override
    public void setVisible(boolean visible) { this.visible = visible; }
    
    @Override
    public UIComponent findById(String id) {
        return this.id.equals(id) ? this : null;
    }
    
    @Override
    public List<UIComponent> findByType(Class<? extends UIComponent> type) {
        List<UIComponent> result = new ArrayList<>();
        if (type.isInstance(this)) {
            result.add(this);
        }
        return result;
    }
    
    public void setActionListener(ActionListener listener) {
        this.actionListener = listener;
    }
    
    public String getText() { return text; }
    public String getId() { return id; }
}

class Label implements UIComponent {
    private String id;
    private String text;
    private Dimension size;
    private boolean visible = true;
    
    public Label(String id, String text) {
        this.id = id;
        this.text = text;
        this.size = new Dimension(text.length() * 8, 20);
    }
    
    @Override
    public void render() {
        if (visible) {
            System.out.println("Rendering Label: " + text + " [" + size.width + "x" + size.height + "]");
        }
    }
    
    @Override
    public void handleEvent(UIEvent event) {
        // Labels typically don't handle events
    }
    
    @Override
    public Dimension getSize() { return new Dimension(size); }
    
    @Override
    public void setSize(Dimension size) { this.size = new Dimension(size); }
    
    @Override
    public boolean isVisible() { return visible; }
    
    @Override
    public void setVisible(boolean visible) { this.visible = visible; }
    
    @Override
    public UIComponent findById(String id) {
        return this.id.equals(id) ? this : null;
    }
    
    @Override
    public List<UIComponent> findByType(Class<? extends UIComponent> type) {
        List<UIComponent> result = new ArrayList<>();
        if (type.isInstance(this)) {
            result.add(this);
        }
        return result;
    }
    
    public String getText() { return text; }
    public void setText(String text) { this.text = text; }
    public String getId() { return id; }
}

// Composite container
class Panel implements UIComponent {
    private String id;
    private List<UIComponent> children;
    private Dimension size;
    private boolean visible = true;
    private LayoutManager layout;
    
    public Panel(String id) {
        this.id = id;
        this.children = new ArrayList<>();
        this.size = new Dimension(300, 200);
        this.layout = new FlowLayout();
    }
    
    @Override
    public void render() {
        if (visible) {
            System.out.println("Rendering Panel: " + id + " [" + size.width + "x" + size.height + "]");
            layout.layoutComponents(children, size);
            for (UIComponent child : children) {
                child.render();
            }
        }
    }
    
    @Override
    public void handleEvent(UIEvent event) {
        for (UIComponent child : children) {
            child.handleEvent(event);
        }
    }
    
    @Override
    public Dimension getSize() { return new Dimension(size); }
    
    @Override
    public void setSize(Dimension size) { 
        this.size = new Dimension(size);
        layout.layoutComponents(children, size);
    }
    
    @Override
    public boolean isVisible() { return visible; }
    
    @Override
    public void setVisible(boolean visible) { 
        this.visible = visible;
        for (UIComponent child : children) {
            child.setVisible(visible && child.isVisible());
        }
    }
    
    @Override
    public UIComponent findById(String id) {
        if (this.id.equals(id)) {
            return this;
        }
        
        for (UIComponent child : children) {
            UIComponent found = child.findById(id);
            if (found != null) {
                return found;
            }
        }
        return null;
    }
    
    @Override
    public List<UIComponent> findByType(Class<? extends UIComponent> type) {
        List<UIComponent> result = new ArrayList<>();
        if (type.isInstance(this)) {
            result.add(this);
        }
        
        for (UIComponent child : children) {
            result.addAll(child.findByType(type));
        }
        return result;
    }
    
    // Composite-specific operations
    public void add(UIComponent component) {
        children.add(component);
        layout.layoutComponents(children, size);
    }
    
    public void remove(UIComponent component) {
        children.remove(component);
        layout.layoutComponents(children, size);
    }
    
    public List<UIComponent> getChildren() {
        return new ArrayList<>(children);
    }
    
    public void setLayout(LayoutManager layout) {
        this.layout = layout;
        layout.layoutComponents(children, size);
    }
    
    public String getId() { return id; }
}

// Supporting classes
class Dimension {
    public int width, height;
    
    public Dimension(int width, int height) {
        this.width = width;
        this.height = height;
    }
    
    public Dimension(Dimension other) {
        this.width = other.width;
        this.height = other.height;
    }
}

class UIEvent {
    public enum Type { CLICK, HOVER, KEY_PRESS }
    
    private Type type;
    private String target;
    
    public UIEvent(Type type, String target) {
        this.type = type;
        this.target = target;
    }
    
    public Type getType() { return type; }
    public String getTarget() { return target; }
}

interface ActionListener {
    void actionPerformed();
}

interface LayoutManager {
    void layoutComponents(List<UIComponent> components, Dimension containerSize);
}

class FlowLayout implements LayoutManager {
    @Override
    public void layoutComponents(List<UIComponent> components, Dimension containerSize) {
        // Simple flow layout implementation
        int x = 10, y = 10;
        int maxHeight = 0;
        
        for (UIComponent component : components) {
            Dimension componentSize = component.getSize();
            
            if (x + componentSize.width > containerSize.width - 10) {
                x = 10;
                y += maxHeight + 10;
                maxHeight = 0;
            }
            
            // In real implementation, would set component position
            x += componentSize.width + 10;
            maxHeight = Math.max(maxHeight, componentSize.height);
        }
    }
}

Example 3: Mathematical Expression Tree

// Component interface for mathematical expressions
interface Expression {
    double evaluate();
    String toString();
    Expression simplify();
    Expression derivative(String variable);
    Set<String> getVariables();
}

// Leaf expression - Number
class Number implements Expression {
    private double value;
    
    public Number(double value) {
        this.value = value;
    }
    
    @Override
    public double evaluate() {
        return value;
    }
    
    @Override
    public String toString() {
        return String.valueOf(value);
    }
    
    @Override
    public Expression simplify() {
        return this; // Numbers are already simplified
    }
    
    @Override
    public Expression derivative(String variable) {
        return new Number(0); // Derivative of constant is 0
    }
    
    @Override
    public Set<String> getVariables() {
        return new HashSet<>(); // Numbers have no variables
    }
    
    public double getValue() { return value; }
}

// Leaf expression - Variable
class Variable implements Expression {
    private String name;
    private Map<String, Double> values;
    
    public Variable(String name) {
        this.name = name;
        this.values = new HashMap<>();
    }
    
    public void setValue(String variable, double value) {
        values.put(variable, value);
    }
    
    @Override
    public double evaluate() {
        Double value = values.get(name);
        if (value == null) {
            throw new IllegalStateException("Value not set for variable: " + name);
        }
        return value;
    }
    
    @Override
    public String toString() {
        return name;
    }
    
    @Override
    public Expression simplify() {
        return this; // Variables are already simplified
    }
    
    @Override
    public Expression derivative(String variable) {
        return new Number(variable.equals(name) ? 1 : 0);
    }
    
    @Override
    public Set<String> getVariables() {
        Set<String> variables = new HashSet<>();
        variables.add(name);
        return variables;
    }
    
    public String getName() { return name; }
}

// Composite expression - Binary Operation
class BinaryOperation implements Expression {
    public enum Operator { ADD, SUBTRACT, MULTIPLY, DIVIDE, POWER }
    
    private Expression left;
    private Expression right;
    private Operator operator;
    
    public BinaryOperation(Expression left, Operator operator, Expression right) {
        this.left = left;
        this.operator = operator;
        this.right = right;
    }
    
    @Override
    public double evaluate() {
        double leftValue = left.evaluate();
        double rightValue = right.evaluate();
        
        switch (operator) {
            case ADD: return leftValue + rightValue;
            case SUBTRACT: return leftValue - rightValue;
            case MULTIPLY: return leftValue * rightValue;
            case DIVIDE: 
                if (rightValue == 0) throw new ArithmeticException("Division by zero");
                return leftValue / rightValue;
            case POWER: return Math.pow(leftValue, rightValue);
            default: throw new UnsupportedOperationException("Unknown operator: " + operator);
        }
    }
    
    @Override
    public String toString() {
        String op = getOperatorSymbol();
        return "(" + left.toString() + " " + op + " " + right.toString() + ")";
    }
    
    @Override
    public Expression simplify() {
        Expression simplifiedLeft = left.simplify();
        Expression simplifiedRight = right.simplify();
        
        // If both operands are numbers, evaluate the expression
        if (simplifiedLeft instanceof Number && simplifiedRight instanceof Number) {
            double result = new BinaryOperation(simplifiedLeft, operator, simplifiedRight).evaluate();
            return new Number(result);
        }
        
        // Apply specific simplification rules
        return applySimplifcationRules(simplifiedLeft, simplifiedRight);
    }
    
    private Expression applySimplifcationRules(Expression left, Expression right) {
        // Addition rules
        if (operator == Operator.ADD) {
            if (left instanceof Number && ((Number) left).getValue() == 0) return right;
            if (right instanceof Number && ((Number) right).getValue() == 0) return left;
        }
        
        // Multiplication rules
        if (operator == Operator.MULTIPLY) {
            if (left instanceof Number && ((Number) left).getValue() == 0) return new Number(0);
            if (right instanceof Number && ((Number) right).getValue() == 0) return new Number(0);
            if (left instanceof Number && ((Number) left).getValue() == 1) return right;
            if (right instanceof Number && ((Number) right).getValue() == 1) return left;
        }
        
        return new BinaryOperation(left, operator, right);
    }
    
    @Override
    public Expression derivative(String variable) {
        Expression leftDeriv = left.derivative(variable);
        Expression rightDeriv = right.derivative(variable);
        
        switch (operator) {
            case ADD:
            case SUBTRACT:
                return new BinaryOperation(leftDeriv, operator, rightDeriv);
                
            case MULTIPLY:
                // Product rule: (uv)' = u'v + uv'
                Expression term1 = new BinaryOperation(leftDeriv, Operator.MULTIPLY, right);
                Expression term2 = new BinaryOperation(left, Operator.MULTIPLY, rightDeriv);
                return new BinaryOperation(term1, Operator.ADD, term2);
                
            case DIVIDE:
                // Quotient rule: (u/v)' = (u'v - uv') / v²
                Expression numerator1 = new BinaryOperation(leftDeriv, Operator.MULTIPLY, right);
                Expression numerator2 = new BinaryOperation(left, Operator.MULTIPLY, rightDeriv);
                Expression numerator = new BinaryOperation(numerator1, Operator.SUBTRACT, numerator2);
                Expression denominator = new BinaryOperation(right, Operator.POWER, new Number(2));
                return new BinaryOperation(numerator, Operator.DIVIDE, denominator);
                
            default:
                throw new UnsupportedOperationException("Derivative not implemented for: " + operator);
        }
    }
    
    @Override
    public Set<String> getVariables() {
        Set<String> variables = new HashSet<>();
        variables.addAll(left.getVariables());
        variables.addAll(right.getVariables());
        return variables;
    }
    
    private String getOperatorSymbol() {
        switch (operator) {
            case ADD: return "+";
            case SUBTRACT: return "-";
            case MULTIPLY: return "*";
            case DIVIDE: return "/";
            case POWER: return "^";
            default: return "?";
        }
    }
    
    public Expression getLeft() { return left; }
    public Expression getRight() { return right; }
    public Operator getOperator() { return operator; }
}

Example 4: Application Usage

public class CompositePatternDemo {
    public static void demonstrateFileSystem() {
        // Create file system structure
        Directory root = new Directory("root");
        Directory documents = new Directory("Documents");
        Directory photos = new Directory("Photos");
        
        File readme = new File("README.txt", "This is a readme file");
        File config = new File("config.json", "{}");
        File photo1 = new File("vacation.jpg", 2048000);
        File photo2 = new File("family.png", 1536000);
        
        // Build hierarchy
        root.add(documents);
        root.add(photos);
        documents.add(readme);
        documents.add(config);
        photos.add(photo1);
        photos.add(photo2);
        
        // Demonstrate uniform operations
        System.out.println("=== File System Structure ===");
        root.display("");
        
        System.out.println("\n=== Size Information ===");
        System.out.println("Total size: " + root.getSize() + " bytes");
        
        System.out.println("\n=== Search Operations ===");
        FileSystemComponent found = root.findByName("config.json");
        if (found != null) {
            System.out.println("Found: " + found.getName());
        }
        
        List<FileSystemComponent> jpgFiles = root.findByExtension("jpg");
        System.out.println("JPG files found: " + jpgFiles.size());
        
        System.out.println("\n=== Visitor Pattern ===");
        SizeCalculatorVisitor sizeVisitor = new SizeCalculatorVisitor();
        root.accept(sizeVisitor);
        System.out.println("Total files: " + sizeVisitor.getFileCount());
        System.out.println("Total directories: " + sizeVisitor.getDirectoryCount());
        System.out.println("Total size: " + sizeVisitor.getTotalSize() + " bytes");
    }
    
    public static void demonstrateUI() {
        // Create UI structure
        Panel mainPanel = new Panel("main");
        Panel buttonPanel = new Panel("buttons");
        Panel labelPanel = new Panel("labels");
        
        Button okButton = new Button("ok", "OK");
        Button cancelButton = new Button("cancel", "Cancel");
        Label titleLabel = new Label("title", "Application Title");
        Label statusLabel = new Label("status", "Ready");
        
        // Build hierarchy
        mainPanel.add(labelPanel);
        mainPanel.add(buttonPanel);
        labelPanel.add(titleLabel);
        labelPanel.add(statusLabel);
        buttonPanel.add(okButton);
        buttonPanel.add(cancelButton);
        
        // Demonstrate uniform operations
        System.out.println("\n=== UI Rendering ===");
        mainPanel.render();
        
        System.out.println("\n=== Event Handling ===");
        UIEvent clickEvent = new UIEvent(UIEvent.Type.CLICK, "ok");
        mainPanel.handleEvent(clickEvent);
        
        System.out.println("\n=== Component Search ===");
        UIComponent foundComponent = mainPanel.findById("status");
        if (foundComponent != null) {
            System.out.println("Found component: " + foundComponent.getClass().getSimpleName());
        }
        
        List<UIComponent> buttons = mainPanel.findByType(Button.class);
        System.out.println("Buttons found: " + buttons.size());
    }
    
    public static void demonstrateMathExpressions() {
        // Create mathematical expression: (x + 2) * (x - 1)
        Variable x = new Variable("x");
        x.setValue("x", 3.0);
        
        Expression expr1 = new BinaryOperation(x, BinaryOperation.Operator.ADD, new Number(2));
        Expression expr2 = new BinaryOperation(x, BinaryOperation.Operator.SUBTRACT, new Number(1));
        Expression mainExpr = new BinaryOperation(expr1, BinaryOperation.Operator.MULTIPLY, expr2);
        
        System.out.println("\n=== Mathematical Expression ===");
        System.out.println("Expression: " + mainExpr.toString());
        System.out.println("Evaluation (x=3): " + mainExpr.evaluate());
        
        System.out.println("\n=== Simplification ===");
        Expression simplified = mainExpr.simplify();
        System.out.println("Simplified: " + simplified.toString());
        
        System.out.println("\n=== Derivative ===");
        Expression derivative = mainExpr.derivative("x");
        System.out.println("Derivative: " + derivative.toString());
        Expression simplifiedDeriv = derivative.simplify();
        System.out.println("Simplified derivative: " + simplifiedDeriv.toString());
    }
    
    public static void main(String[] args) {
        demonstrateFileSystem();
        demonstrateUI();
        demonstrateMathExpressions();
    }
}

6. Hands-On Practice (30-40 minutes)

Exercise 1: Basic Practice (10 minutes)

Objective: Implement a simple composite for organizational hierarchy

Instructions:

  1. Create an Employee interface with methods: getName(), getSalary(), getRole()
  2. Implement Individual (leaf) and Department (composite) classes
  3. Add operations to calculate total salary and count employees
  4. Test with a multi-level organizational structure

Starter Code:

interface Employee {
    String getName();
    double getSalary();
    String getRole();
    double getTotalSalary();
    int getEmployeeCount();
    void displayHierarchy(String indent);
}

// TODO: Implement Individual and Department classes

Exercise 2: Intermediate Challenge (15 minutes)

Objective: Build a document structure with formatting operations

Requirements:

  • Create components for Text, Paragraph, Section, Document
  • Implement operations: render(), getWordCount(), applyStyle()
  • Support nested sections and mixed content types
  • Add search functionality to find text within the structure

Hints:

  • Consider how styles cascade through the hierarchy
  • Think about how word counting should work for composites
  • Design the search to work at any level of the hierarchy

Exercise 3: Creative Application (15 minutes)

Objective: Design a composite for a shopping cart system

Challenge: Create a composite structure for:

  • Individual products with price and quantity
  • Product bundles that contain other products or bundles
  • Implement operations: getTotalPrice(), getItemCount(), applyDiscount()
  • Support percentage and fixed-amount discounts at any level

Extension Ideas:

  • Add tax calculation that respects product categories
  • Implement shipping cost calculation based on weight/size
  • Add inventory checking that works recursively

7. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Mistakes

  1. Problem: Not implementing uniform interface correctly Cause: Adding composite-specific methods to the component interface Solution: Keep component interface focused on operations that apply to both leaves and composites

  2. Problem: Inefficient recursive operations Cause: Not optimizing traversal or caching results Solution: Use visitor pattern for complex operations, cache expensive calculations

  3. Problem: Memory leaks with circular references Cause: Parent and child objects referencing each other inappropriately Solution: Be careful with bidirectional references, use weak references where appropriate

Debugging Tips

  • Use proper indentation in display methods to visualize structure
  • Implement equals() and hashCode() for proper collection behavior
  • Add logging to recursive operations to trace execution
  • Use debugger to visualize object relationships and hierarchy

8. Best Practices and Professional Tips (5 minutes)

Industry Standards

  • Keep interfaces uniform - avoid exposing composite-specific operations in component interface
  • Use visitor pattern for complex operations that don’t belong in the core interface
  • Cache expensive calculations - total size, counts, etc. can be cached and invalidated when structure changes
  • Handle edge cases - null children, empty composites, circular references

Design Principles

  • Prefer composition over inheritance - use interfaces rather than abstract classes when possible
  • Make operations idempotent when possible - multiple calls should be safe
  • Design for immutability when feasible - reduces complexity of caching and thread safety
  • Use factory methods for creating complex composite structures

Performance Considerations

  • Avoid deep recursion for very large trees - consider iterative approaches
  • Implement lazy loading for expensive child operations
  • Use appropriate data structures - ArrayList vs LinkedList based on access patterns
  • Consider using flyweight pattern for shared leaf objects

Next Steps in Your Learning Journey

  • Study Visitor pattern for separating operations from object structure
  • Learn Iterator pattern for traversing composite structures
  • Explore Flyweight pattern for sharing leaf objects efficiently
  • Investigate tree algorithms for searching and optimization

9. Summary and Review (5 minutes)

Key Takeaways

  1. Composite pattern treats individual and group objects uniformly through common interface
  2. Perfect for hierarchical structures where you need to perform same operations on parts and wholes
  3. Recursion is central to composite pattern implementation
  4. Choose appropriate transparency vs safety trade-offs based on your use case

Quick Review Questions

  1. What’s the difference between transparent and safe composite design?
  2. When would you use Visitor pattern with Composite pattern?
  3. How do you handle performance issues with deep composite structures?

For Instructors

Teaching Tips

  • Start with familiar hierarchies (file systems, organizational charts) before technical examples
  • Draw tree diagrams on whiteboard to visualize structure and operations
  • Show both simple and complex examples to demonstrate scalability
  • Emphasize uniform interface concept - this is the key insight
  • Live code the recursive operations to show the pattern in action

Common Student Questions

  • “When to use Composite vs simple inheritance?” - Explain when you need uniform treatment of groups
  • “How deep can composite structures go?” - Discuss performance implications and practical limits
  • “Should all operations be recursive?” - Show examples where only some operations recurse

Assessment Ideas

  • Design challenge: Create composite for specific domain (music playlists, recipe ingredients)
  • Performance analysis: Compare recursive vs iterative traversal approaches
  • Refactoring exercise: Convert existing hierarchy to use composite pattern

Adaptation Notes

  • Beginner focus: Start with simple two-level hierarchies before complex nested structures
  • Advanced students: Discuss memory optimization techniques and concurrent access patterns
  • Language-specific: Show language-specific optimizations (streams, lambdas, etc.)
  • Time management: Mathematical expression example can be simplified or extended based on available time