The Composite Pattern 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.
It is the go-to solution whenever your data has a hierarchical "part-whole" relationship, like folders containing files and other folders.
The Problem
Imagine you are building a File System module for your website. You have two types of objects: Files and Folders.
- A File has a size.
- A Folder contains many Files... but it can also contain other Folders.
If you want to calculate the total size of a folder, you have to check every item. If it's a file, you get the size. If it's a folder, you have to look inside that folder and repeat the process. Without the Composite pattern, your code becomes a mess of if-else checks to see if an object is a File or a Folder.
The Solution
We treat Files and Folders the same way by having them implement a common interface. The "Composite" (Folder) delegates the work to its children, while the "Leaf" (File) actually performs the work.
Step-by-Step Java Implementation
1. The Component Interface
This defines the common operations for both simple and complex objects.
// FileSystemComponent.java
public interface FileSystemComponent {
void showDetails();
long getSize();
}
2. The Leaf (Simple Object)
The leaf represents the end-objects of a composition. A leaf can't have any children.
// File.java
public class File implements FileSystemComponent {
private String name;
private long size;
public File(String name, long size) {
this.name = name;
this.size = size;
}
public void showDetails() {
System.out.println("File: " + name + " (" + size + " KB)");
}
public long getSize() {
return size;
}
}
3. The Composite (Complex Object)
The composite stores child components and implements the interface methods by delegating the work to those children.
// Folder.java
import java.util.ArrayList;
import java.util.List;
public class Folder implements FileSystemComponent {
private String name;
private List<FileSystemComponent> components = new ArrayList<>();
public Folder(String name) {
this.name = name;
}
public void addComponent(FileSystemComponent component) {
components.add(component);
}
public void showDetails() {
System.out.println("Folder: [" + name + "]");
for (FileSystemComponent component : components) {
component.showDetails();
}
}
public long getSize() {
long totalSize = 0;
for (FileSystemComponent component : components) {
totalSize += component.getSize();
}
return totalSize;
}
}
Full Code for Testing
// Save as CompositeTest.java
import java.util.ArrayList;
import java.util.List;
// 1. Component
interface FileSystemComponent {
void showDetails();
long getSize();
}
// 2. Leaf
class File implements FileSystemComponent {
private String name;
private long size;
public File(String name, long size) {
this.name = name;
this.size = size;
}
public void showDetails() {
System.out.println(" - File: " + name + " (" + size + " KB)");
}
public long getSize() { return size; }
}
// 3. Composite
class Folder implements FileSystemComponent {
private String name;
private List<FileSystemComponent> children = new ArrayList<>();
public Folder(String name) { this.name = name; }
public void add(FileSystemComponent component) {
children.add(component);
}
public void showDetails() {
System.out.println("Folder: " + name);
for (FileSystemComponent child : children) {
child.showDetails();
}
}
public long getSize() {
int total = 0;
for (FileSystemComponent child : children) {
total += child.getSize();
}
return total;
}
}
// 4. Main
public class CompositeTest {
public static void main(String[] args) {
System.out.println("--- Composite Pattern Test ---\n");
// Create individual files
File resume = new File("resume.pdf", 500);
File photo = new File("profile.jpg", 1200);
File script = new File("index.js", 15);
// Create a sub-folder
Folder docs = new Folder("Documents");
docs.add(resume);
// Create a root folder
Folder root = new Folder("Root");
root.add(docs);
root.add(photo);
root.add(script);
// Treat the root folder and a single file exactly the same
root.showDetails();
System.out.println("\nTotal Storage Used: " + root.getSize() + " KB");
System.out.println("\n--- End of Test ---");
}
}
Why use Composite?
- Uniformity: The client doesn't need to know if it's dealing with a single object or a collection. It calls the same method (
getSize()) regardless. - Scalability: You can add new types of components (like a
ShortcutorArchive) without changing the code that uses the tree. - Clean Code: It removes complex conditional logic and recursion from your client code and hides it inside the classes.