What is Composite Pattern?
Composite pattern is a structural pattern that allows you to compose objects into tree structures to represent whole-part hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.
In Composite pattern, elements with child elements are called nodes and elements without children are called leaves.
When to use it?
- Use Composite pattern when you need whole-part or parent-child or tree-like hierarchy.
- Use Composite pattern when you want to let client to treat child and parent in a tree uniformly.
Problem
We are trying to implement simple file system. First of all, we need File
and Directory
classes. But it will be difficult for client to keep checking which instances of classes each object is as our file structure grows bigger.
File system is a perfect example of tree-like hierarchy, and we want to treat File
and Directory
uniformly. Time to use Composite pattern!
Solution
Client
Client doesn't care whether an object is instance ofFile
orDirectory
thanks toFileSystemComponent
. Additionally, client doesn't have to write if statements to make sure whether he is calling right methods on the right objects.FileSystemComponent
File
andDirectory
are seen asFileSystemComponent
by Client.FileSystemComponent
defines default behavior for methods, default behavior can be exception, doing nothing, returning null or false, whatever makes sense for your application.File
This is our leaf, overriding print method that prints itsname
andcontent
.Directory
This is our node, overriding methods to add, remove, get children.print
method prints out itsname
and calls children'sprint
methods as well so that Client doesn't need to call each component'sprint
method.
Structure
Implementation in Java
public abstract class FileSystemComponent {
protected String name;
public FileSystemComponent(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void print(int indentLevel) {
throw new UnsupportedOperationException();
}
public void add(FileSystemComponent component) {
throw new UnsupportedOperationException();
}
public void remove(int index) {
throw new UnsupportedOperationException();
}
// Instead of throwing exception, we do nothing by default.
// Doing nothing makes sense because leaf has no child.
public void getChildren() {
}
}
public class File extends FileSystemComponent {
private String content;
public File(String name, String content) {
super(name);
this.content = content;
}
@Override
public void print(int indentLevel) {
String indent = " ".repeat(indentLevel);
System.out.println(indent + name + ": " + content);
}
}
public class Directory extends FileSystemComponent {
private List<FileSystemComponent> children;
public Directory(String name) {
super(name);
children = new ArrayList<>();
}
@Override
public void print(int indentLevel) {
String indent = " ".repeat(indentLevel);
System.out.println(indent + name + " directory:");
for (FileSystemComponent child : children) {
child.print(indentLevel + 2);
}
}
@Override
public void add(FileSystemComponent component) {
children.add(component);
}
@Override
public void remove(int index) {
children.remove(index);
}
@Override
public void getChildren() {
if (children.isEmpty()) {
return;
}
StringBuilder builder = new StringBuilder("[");
for (FileSystemComponent child : children) {
builder.append(child.getName() + ", ");
}
builder.delete(builder.length() - 2, builder.length());
builder.append("]");
System.out.println(builder);
}
}
public class FileSystemTestDrive {
public static void main(String[] args) {
FileSystemComponent rootDirectory = new Directory("Root");
FileSystemComponent fruitsDirectory = new Directory("Fruits");
FileSystemComponent animalDirectory = new Directory("Animal");
FileSystemComponent felineDirectory = new Directory("Feline");
rootDirectory.add(fruitsDirectory);
rootDirectory.add(animalDirectory);
fruitsDirectory.add(new File("Appple", "red and juicy."));
fruitsDirectory.add(new File("Banana", "yellow and sweet."));
fruitsDirectory.add(new File("Lemon", "yellow and sour."));
animalDirectory.add(felineDirectory);
felineDirectory.add(new File("Lion", "King of animal."));
felineDirectory.add(new File("Tiger", "Has cool color pattern."));
rootDirectory.print(0);
rootDirectory.getChildren();
rootDirectory.remove(0);
rootDirectory.getChildren();
// What happens we call getChildren() on leaf? (we don't override the method in leaf class)
FileSystemComponent file = new File("sample", "this is leaf");
file.getChildren(); // leaf calls default behavior, doing nothing
}
}
Output:
Root directory:
Fruits directory:
Appple: red and juicy.
Banana: yellow and sweet.
Lemon: yellow and sour.
Animal directory:
Feline directory:
Lion: King of animal.
Tiger: Has cool color pattern.
[Fruits, Animal]
[Animal]
Pitfalls
- If you want a composite that keeps its children in particular order, it requires more sophisticated management scheme for adding, removing and traversing children.
- As composite structure becomes larger and complex, it would be more expensive to traverse. In this case, you might consider to implement a cache that stores some data to save traversals.
You can check all the design pattern implementations here.
GitHub Repository
Top comments (0)