Composite

Structural Design Patterns

Imagine you are building a tool to analyze a file system. A file system is a natural tree structure: folders can contain files and other folders. A common task is to calculate the total size of everything within a given folder.

Without a unifying pattern, your client code would become a tangled mess of if/else statements and explicit recursion:

public class FileSystemAnalyzer {
    public long calculateSize(Object item) {
        long totalSize = 0;
        if (item instanceof Folder) {
            Folder folder = (Folder) item;
            for (Object child : folder.getChildren()) {
                // Recursive call for sub-folders, different logic for files
                if (child instanceof Folder) {
                    totalSize += calculateSize(child); // Recursive call
                } else if (child instanceof File) {
                    totalSize += ((File) child).getSize();
                }
            }
        } else if (item instanceof File) {
            totalSize += ((File) item).getSize();
        }
        return totalSize;
    }
}

This code is complex, hard to read, and brittle. It violates the Open/Closed Principle because if you add a new type of file system object, like a SymbolicLink, you have to go back and change the calculateSize method. The client is forced to know the difference between individual items (File) and collections of items (Folder).

Treating Objects and Compositions Uniformly

The Composite is a structural design pattern that lets you compose objects into tree structures and then work with these structures as if they were individual objects.

The core idea is to create a common interface for both individual objects (called "leaves") and container objects (called "composites"). This allows the client to be completely ignorant of the difference between a single object and a group of objects. The client can call the same method on a single leaf or on a complex composite structure, and the operation just works. The complexity of traversing the tree is hidden within the composite objects themselves.

The Participants

  1. Component: The interface or abstract class that provides a common contract for all objects in the tree, both leaves and composites. It declares the common operations (e.g., getSize()).
  2. Leaf: The basic, individual object in the composition. It implements the Component interface but has no children. It provides the base implementation for the primitive objects in the tree (e.g., a File).
  3. Composite: The container object that can have children. It also implements the Component interface. It stores a collection of child Component objects. Its implementation of the common operations usually involves delegating the work to its children and then aggregating the results.

Example: The File System

Let's refactor our file system analysis using the Composite pattern.

Step 1: The Component Interface This is the unifying contract for both files and folders.

public interface FileSystemComponent {
    String getName();
    int getSizeInKB();
}

Step 2: The Leaf Class A File is a leaf; it has a size but no children.

public class File implements FileSystemComponent {
    private String name;
    private int sizeInKB;

    public File(String name, int sizeInKB) { /* ... constructor ... */ }

    @Override public String getName() { return this.name; }
    @Override public int getSizeInKB() { return this.sizeInKB; }
}

Step 3: The Composite Class A Folder is a composite. Its getSizeInKB method recursively sums the size of its children.

import java.util.ArrayList;
import java.util.List;

public class Folder implements FileSystemComponent {
    private String name;
    private List<FileSystemComponent> children = new ArrayList<>();

    public Folder(String name) { /* ... constructor ... */ }
    
    public void addComponent(FileSystemComponent component) {
        children.add(component);
    }

    @Override public String getName() { return this.name; }

    @Override
    public int getSizeInKB() {
        int totalSize = 0;
        // The magic of recursion! The folder doesn't care if a child
        // is a File or another Folder. It just calls the common method.
        for (FileSystemComponent component : children) {
            totalSize += component.getSizeInKB();
        }
        return totalSize;
    }
}

Step 4: The Simplified Client Code The client can now build a tree and operate on it uniformly.

// Build the tree structure
Folder root = new Folder("root");
Folder documents = new Folder("documents");
File doc1 = new File("report.docx", 100);
File doc2 = new File("notes.txt", 10);

documents.addComponent(doc1);
documents.addComponent(doc2);
root.addComponent(documents);

File picture = new File("family_photo.jpg", 500);
root.addComponent(picture);

// The client code is now incredibly simple.
System.out.println("Total size of root folder: " + root.getSizeInKB() + "KB");
// Output: Total size of root folder: 610KB

Interview Focus: Analysis and Trade-offs

Benefits:

  • Uniformity and Simplicity: The client code is dramatically simplified because it doesn't need to differentiate between individual objects and containers.
  • Open/Closed Principle: The pattern makes it easy to add new kinds of components (new leaf or composite types) to the system without changing the client code, as long as they adhere to the common Component interface.

How Composite Relates to Other Concepts

  • The Go-To for Tree Structures: This is a very apt pattern for any problem that can be modeled as a part-whole hierarchy. This includes UI component trees (a Panel contains Buttons and other Panels), Abstract Syntax Trees (ASTs) in compilers, and Document Object Models (DOM).
  • Enabling Recursion: The power of the Composite pattern lies in its natural support for recursion. Operations on the composite are cleanly and recursively delegated down the tree until they reach the leaf nodes.
  • Relationship with Other Patterns:
    • Decorator: While a Decorator adds responsibilities to a single object, a Composite is about composing a group of objects. You can, however, use a Decorator to add functionality to an entire Composite structure at once.
    • Iterator and Visitor: These behavioral patterns are often used in conjunction with the Composite pattern to provide different ways of traversing the tree structure.