Composite design pattern is a structural design pattern which allows you to use group of objects as a single object. This creates a tree like structure or directory like structure where composite objects can contain simple objects (leaf) as well as composite objects (branches). This tutorial explains this pattern with examples.

What is Composite Pattern?

This is a design pattern for composing nested structures of objects and dealing with these objects in a uniform manner.

When we have a situation where our core model can be represented as a tree, we can delegate the work to hierarchy and get the results. This usually ends up creating a tree like structure where we have branches and eventually end nodes which are called leaf nodes. For example, in directory structure, we can have directories of directories as well as we can have files inside a directory. If we want to get the size of the directory, we can traverse its branches recursively and get size of all files and sum them up. In this case, we want directory (branches) and files (leaf nodes) to behave similar.

Similarly, if you want to build a drawing app, you might have CompositeShape which consists of different shapes. You can have Shape interface which defines the draw() method for all shapes. You create hierarchy of shapes as Circle and Square which are also leaf classes and provide specific implementation of draw() method. CompositeShape might consist of multiple composite shapes or leaf classes like Square or Circle. So, it has methods like addShape() which will add more children to draw. When the draw() method is called on CompositeShape, it will simply iterate through its child shapes and delegate the drawing to those child shape draw() method. The client code can consider them as shape using their draw() method.

The same can be thought for hierarchy of tasks. For example, you might have seen bullet lists consisting of other bullet lists. They can be treated as comoposite objects. The same kind of logic applies for JIRA project management EPICs.

How to implement Composite Pattern?

The Composite design pattern consists of following components.

  • Component: It defines the common operations that are common to simple and complex items of the tree hierarchy.
  • Leaf Node: The leaf will be the most basic element of the tree that cannot be sub-divided.
  • Container: The container is also the composite object or complex object which might consist of other complex objects or leaf objects. A container has collection of component. Whenever a request is made to container object, it simply delegates the task to children component.

To implement this pattern do the following.

  1. Identify the Leaf class and container class and identify the common operations.
  2. Define common interface Component for both leaf class and complex class which only declares those methods common to both.
  3. Implement this common interface for all possible Leaf classes and complex container classes.
  4. Container class must also have a method to add new child classes of original Component.
  5. The leaf classes must have concrete implementation for those methods doing the actual work. On the other hand, container classes should delegate the work to its child components.

Task example

Below is a practical example with SubTask as Leaf class and Task as container class.

First declare common methods in TaskItem interface.

1public interface TaskItem {
2    public String getName();
3    public boolean isCompleted();
4    public void complete();
5    public void listItems();
6}

Implement the Leaf class SubTask. This class actually does the work.

 1public class SubTask implements TaskItem {
 2    private String name;
 3    private boolean completed;
 4
 5    public SubTask(String name) {
 6        this.name = name;
 7        this.completed = false;
 8    }
 9
10    @Override
11    public String getName() {
12        return name;
13    }
14
15    @Override
16    public boolean isCompleted() {
17        return completed;
18    }
19
20    @Override
21    public void complete() {
22        completed = true;
23    }
24
25    public void listItems() {
26        System.out.println("SubTask: " + getName() + " is " + (isCompleted() ? "completed" : "not completed"));
27    }
28}

Define container class or composite class which consists of list of Component objects. This works with common interface and not the actual implementation.

 1public class Task implements TaskItem {
 2    private String name;
 3    private List<SubTask> tasks;
 4
 5    public Task(String name) {
 6        this.name = name;
 7        this.tasks = new ArrayList<>();
 8    }
 9
10    public void addSubTask(SubTask task) {
11        tasks.add(task);
12    }
13
14    @Override
15    public String getName() {
16        return name;
17    }
18
19    @Override
20    public boolean isCompleted() {
21        return tasks.stream()
22                .allMatch(SubTask::isCompleted);
23    }
24
25    @Override
26    public void complete() {
27        tasks.forEach(SubTask::complete);
28    }
29
30    public void listItems() {
31        System.out.println("Task: " + getName());
32        System.out.println("----------------------------------");
33        for (SubTask task : tasks) {
34            task.listItems();
35        }
36    }
37}

Now, the client can simply treat both these types as being similar and can simply ask for completion or ask to list items. Behind the scenes, Task delegates the work to SubTask.

 1public class Main {
 2    public static void main(String[] args) {
 3        Task task = new Task("Learn Java Programming language");
 4        task.addSubTask(new SubTask("Read Tutorials"));
 5        task.addSubTask(new SubTask("Type the code examples"));
 6        task.addSubTask(new SubTask("Build projects"));
 7
 8        task.listItems();
 9
10
11        System.out.println("==================================");
12        System.out.println("Completing the task");
13        task.complete();
14
15        System.out.println("==================================");
16        task.listItems();
17    }
18}

FileSystem example

Another example is for filesystem as shown below. The FileSystemComponent defines the common operations like getName(), listItems() and getSize()

1public interface FileSystemComponent {
2    public String getName();
3    public void listItems();
4    public int getSize();
5}

The File leaf class implements FileSystemComponent and stores the name and size information for each file.

 1public class File implements FileSystemComponent {
 2    private String name;
 3    private int size;
 4
 5    public File(String name, int size) {
 6        this.name = name;
 7        this.size = size;
 8    }
 9
10    @Override
11    public String getName() {
12        return name;
13    }
14
15    @Override
16    public void listItems() {
17        System.out.println(" File " + getName() + " with size " + getSize() + " bytes");
18    }
19
20    @Override
21    public int getSize() {
22        return size;
23    }
24}

The Directory is a composite class which implements FileSystemComponent and can have list of FileSystemComponents. It also defines addComponent() method to add child items into directory. The listItem() and getSize() methods of Directory class delegates this calls to child objects.

 1public class Directory implements FileSystemComponent {
 2    private String name;
 3    private List<FileSystemComponent> children;
 4
 5    public Directory(String name, List<FileSystemComponent> children) {
 6        this.name = name;
 7        this.children = children;
 8    }
 9
10    public Directory(String name) {
11        this.name = name;
12        this.children = new ArrayList<>();
13    }
14
15    @Override
16    public String getName() {
17        return name;
18    }
19
20    public void addComponent(FileSystemComponent component) {
21        children.add(component);
22    }
23
24    @Override
25    public void listItems() {
26        System.out.println("Directory " + getName());
27        for (FileSystemComponent child : children) {
28            child.listItems();
29        }
30    }
31
32    @Override
33    public int getSize() {
34        return children.stream()
35                .mapToInt(FileSystemComponent::getSize)
36                .sum();
37    }
38}

The client code can treat both File and Directory uniformly using listItems() and getSize() method to get information for the directory or file.

Advantages:

  • This pattern is great for definining hierarchical structure of objects.
  • Each classes are treated the same. Thus it makes the code simple to understand.
  • We can easily add new types of objects without modifying existing code.
  • It follows OCP (Open/Closed principle). That means we can easily extend the functionality of the system by adding new types of objects without modifying existing code.

Disadvantages:

  • There can be performance overhead because of nested lookups. If the structure is very deep, it can be costly traversing the tree recursively.
  • Defining common interface might over simplify it. This means we might end up defining methods which some of the child classes may not need and those may end up having to define empty methods just to implement the common interface.

Comparison with Decorator Pattern

Decorator PatternComposite Pattern
contains a separate entityThis has a tree structure of objects
It modifies the behaviorIt doesn’t modify behavior
It does not change the underlying objectIt creates uniformness between objects
It has one child componentComposite can have any number of children

Summary

  • Composite pattern is designed to treat components the same way.
  • This is implemented by composing objects into a tree like structure.
  • The same operations are applied on individual leaf classes and composite classes.