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).
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.
getSize()).Component interface but has no children. It provides the base implementation for the primitive objects in the tree (e.g., a File).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.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
Benefits:
Component interface.Panel contains Buttons and other Panels), Abstract Syntax Trees (ASTs) in compilers, and Document Object Models (DOM).