Composite Design Pattern Definition
The Composite Design Pattern falls into the Structural Design Patterns. It allows us to compose objects into tree structures to represent part-whole hierarchies. The Composite Design Pattern lets clients treat individual objects and compositions of objects uniformly.
Part-Whole Hierarchy
We deal with objects that have a hierarchical relationship i.e., they have primitive and composite objects. The processing code will be aware of the type of the object (whether it is a primitive or a collection). This means that the client code has to have logic based on the concrete type of the object.
Example:
Let us consider a familiar example - the files and directories in the file system. A file is a primitive object with some content in it. A directory is a composite as it contains various other files and directories within it. This forms the part-whole hierarchy with the file object as the part and a directory taken together with its content forms the whole.
Imagine having these two objects in our application. The client code has to treat these two separately and define operations on them accordingly.
Representing the hierarchy
The Composite Design Pattern lets us treat the hierarchy as a tree that contain both the composition of objects and individual objects as nodes in the tree.
In the above diagram, the nodes 2, 3, 5, and 6 are the primitives. The composites (nodes 1 and 4) are defined in terms of other composites and primitives. Thus, a primitive node does not have a child whereas a composite has one or more children. Each child can either be a primitive or a composite node. This forms a recursive relationship forming a tree.
Treating the part and whole in the same way
The Composite Design Pattern allows us to treat the individual objects (the parts) and the composition of objects (the whole) in the same way.
We do this by creating a common interface or an abstract class for both the primitives (the parts) and their containers (the whole). We call this common interface/super class, the component, and we define the common operation(s) on this common interface. The client (or the caller code) uses this common interface and thus it doesn’t have to deal with the individual (primitive) objects or the composite. We apply the same operation over both the individual nodes and composites. The client does not know about the differences between the primitive node and a composite.
Traversing a directory tree
In our example of files (individual item/node/primitive) and directories (composite), we want to have the print operation. For a child file, it just prints the name of the file. For a directory, it has to print the directory name + print all the contents of that directory. A directory can have files and other directories within it. Hence, we have to traverse the directory recursively.
As we have learnt, the client need not be aware of this difference and hence it does not have to process a file and a directory differently. We create a common interface for a file and a directory and define the operation on that interface. The client calls the print operation on the common component interface. We will implement this operation for both the whole (directory) and part (file).
Let us consider the below directory tree
The part or the individual nodes are the files represented as green blocks. The composites are the directories. A composite can have other primitives or even composites (recursive)
Implementing the Composite Pattern
The File interface is the component interface which has the print operation defined on it.
public interface File {
void print();
}
The SingleFile class represents a concrete (leaf) file on a file system. The print method prints the name of the file.
public class SingleFile implements File {
private final String fileName;
public SingleFile(String fileName) {
this.fileName = fileName;
}
@Override
public void print() {
System.out.println(fileName);
}
}
The Directory class implements the File component but it is a composite. A directory has a list of other File components as its children (this is the key part).
public class Directory implements File {
private final String directoryName;
private final List<File> children;
public Directory(String directoryName, List<File> children) {
this.directoryName = directoryName;
this.children = new ArrayList<>(children);
}
public void addChild(File file) {
this.children.add(file);
}
public void removeChild(File file) {
this.children.add(file);
}
public File getChild(int position) {
if (position < 0 || position >= children.size()) {
throw new RuntimeException("Invalid position " + position);
}
return children.get(position);
}
private int getTotalNumberOfChildren() {
return children.size();
}
@Override
public void print() {
System.out.println("Printing the contents of the directory - " + directoryName);
children.forEach(File::print);
System.out.println("Done printing the contents of the directory - " + directoryName);
}
}
The print method prints the name of the directory, followed by calling the print method on each of its child components.
- If the child is a file, it prints the name of the file and the call returns
- If the child is a composite (a directory), then it recursively calls the print method on its children and so on.
A composite can have other methods that are specific to a composite. Here, we have the following methods.
- addChild: Adds a file as a new child of a directory.
- removeChild: Removes a file from the children list.
- getChild: To get the file component at a given position.
Sidenote: The Directory class is made immutable by making a defensive copy of the passed collection as it is good to strive to make the classes immutable.
Running demo
Let us see this in action
File file1 = new SingleFile("file1.txt");
File file2 = new SingleFile("file2.txt");
File file3 = new SingleFile("file3.txt");
File file4 = new SingleFile("file4.txt");
File file5 = new SingleFile("file5.txt");
Directory directory1 = new Directory("directory1", List.of(file3, file4));
Directory directory2 = new Directory("directory2", List.of(file5));
Directory data = new Directory("data", List.of(directory1, file1, file2, directory2));
data.print();
Note: List.of method is one of the convenience factory methods added to the Collections in Java 9. If you have JDK 8 or lower, use Arrays.asList in place of it.
First, we create the individual node objects (files) for the files 1 to 5. Second, we create the two directories - directory1 with files 3 and 4 as its children and directory2 with file5 as its only child. Finally, we create the root directory with directory 1, directory 2 (composites) and file 1 and file 2 as the children.
Note that the print method is called on the root file directory composite. But to the client it is just a component. This prints:
Printing the contents of the directory - data
Printing the contents of the directory - directory1
file3.txt
file4.txt
Done printing the contents of the directory - directory1
file1.txt
file2.txt
Printing the contents of the directory - directory2
file5.txt
Done printing the contents of the directory - directory2
Done printing the contents of the directory - data
It starts the traversal at data node. At this node, it calls the print method of each of its immediate child (directory1, file1, file2, and directory2 in that order). For each composite, it calls the print of its children and the recursion goes on. I have added a print statement when a composite’s print call to all of its children has completed so that we can pair the beginning and ending print statement for a composite.
Let us add a new file to a directory dynamically
directory2.addChild(new SingleFile("new_file.txt"));
Now calling data.print()
prints,
Printing the contents of the directory - data
Printing the contents of the directory - directory1
file3.txt
file4.txt
Done printing the contents of the directory - directory1
file1.txt
file2.txt
Printing the contents of the directory - directory2
file5.txt
new_file.txt
Done printing the contents of the directory - directory2
Done printing the contents of the directory - data
Structure
Participants
Component (File)
- Interface for the objects in the composition (for both the leaf objects and the composites).
- Implements the default behaviour (if applicable for all objects in the composition).
Leaf (SingleFile)
- Represents the leaf objects in the composition (a primitive object).
- Implements the operation declared in the component.
Composite (Directory)
- Stores child components.
- Can have composite specific methods (for managing its children).
- Implements the operation declared in the component. This is done by calling the operation on each of its children.
Client
- Manipulates the objects in the composition through the component interface
Other variations
In some implementations, you can find the operations defined on the composite (like addChild etc) declared in the component interface or super class itself. But such operations are not applicable for a leaf node and hence I prefer having it in the composite. If you decide to have it in the component, you can have a default implementation to throw an exception. If the component contract is an interface, you can do this by making use of the default methods in the interface.
Related Patterns
- We can use an Iterator to traverse the tree by creating an external iterator.
- The Composite Design Pattern is similar to the Decorator Pattern. A decorator wraps or decorates only one other composite object. A decorator exists to add additional responsibility to an object whereas a Composite holds multiple children and forms a whole of its parts (children).
Conclusion
The Composite Design Pattern helps us define class hierarchies having primitive objects and composites. The clients interact with this hierarchy by a common interface defined as the component. Hence, it can treat composite structures and individual objects uniformly and they do not know if they are working with a leaf or a composite.
References
- Head First Design Patterns: A Brain-Friendly Guide by Eric Freeman and Elisabeth Robson.
- Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides.
- SourceMaking Composite pattern
- Refactoring.guru Composite pattern