Company hierarchies, to-do lists, and magic bags all have at least one thing in common: they can all be represented by tree-like structures! A company has a leader who oversees their subordinates, and each subordinate might have their own subordinates. Similarly, a to-do list has tasks, and some tasks might have sub tasks. Finally, a magic bag has (hopefully magic) items in it, but it can also carry more magic bags, each with their own magic items!

In object oriented programming, a simple way to represent these one-to-many relationships is by using the Composite Pattern.

Assumptions

What is the Composite Pattern?

The composite pattern is a structural design pattern designed to represent tree-like structures, treating leaf and composite classes the same, usually by implementing a component interface in both.

<div style="width:100%; margin:auto;text-align:center;"><img src="https://www.devmaking.com/img/topics/designpatterns/CompositePattern_01.png" alt="composite pattern UML diagram." style="max-width:95%;"> </div>

Treating Leaf and Composite Objects the Same:

Imagine an actual tree; a towering oak with luscious roots, sitting atop a grassy hill on a mid-summer day; there's a slight breeze in the air, gently caressing the sea of leafs.

Tolkienesque prose aside, it has a trunk, branches, and leafs. For simplicity sake, we'll consider the trunk to be a branch as well. A branch (composite) can have more branches, some leafs, or even a mix of both!

<div style="width:100%; margin:auto;text-align:center;"><img src="https://www.devmaking.com/img/topics/designpatterns/CompositePattern_02.png" alt="tree-leaf UML relationship" style="max-width:95%;"> </div>

If parts of the tree are diseased, the best way to ensure that it remains healthy is by "pruning" (cutting off) the unhealthy sections. When we prune, we treat the branches and the leafs the same: cutting off a leaf only results in the leaf being removed. However, if we cut off a branch, all the branches and leafs attached to it are also pruned; the same action can be taken on both without needing to know the specifics of a particular implementation.

Conceptualization

To show the composite pattern in action, we'll create a to-do list that has it's own tasks and sub-lists. When we want to look at the list, we'll read() the contents of the items and sub-lists.

<div style="width:100%; margin:auto;text-align:center;"><img src="https://www.devmaking.com/img/topics/designpatterns/CompositePattern_03.png" alt="todo list composite UML diagram." style="max-width:95%;"> </div>

Let's define a common component interface that our leaf and composite classes will implement:

/* pseudo-code */
// Our component interface:
interface TodoList {
   void read();
 }

Next, we'll create our leaf class, ListItem:

// Our leaf class:
class ListItem implements TodoList {
   String taskDescription;
   
   ListItem(String description) {
     taskDescription = description;
   }
   
   // Implementing the TodoList interface:
   void read() {
     print(taskDescription);
   }
 }

For this example, when we want to read() a list item, it will be printed out to the screen.

Now, let's define our composite class, TaskList. Being the composite, it will have a list that can hold both ListItem and TaskList:

// Our composite class:
class TaskList implements TodoList {
   String taskListName;
   // A list of TodoList items: contains ListItems and TaskLists.
   List&lt;TodoList&gt; todoList = new List&lt;TodoList&gt;();
   
   TaskList(String name) {
     taskListName = name;
   }
   
   // Implementing the TodoList interface:
   void read() {
     // print out the name of our task list:
     print(taskListName);
     // Recursively reading all of the items in the todoList.
     for (task in todoList) {
       task.read();
     }
   }
   
   void add(TodoList task) {
     todoList.add(task);
   }
   
   void remove(TodoList task) {
     todoList.remove(task);
   }
 }

Notice how the read() implementation of TaskList varies from ListItem: when we want to read the task list, we are also going to read all of the list items and sub lists within it. By implementing a common interface, we can treat the two classes the same without needing to know how their implementation works!

With all of the classes implemented, we can have a test run by creating a daily to-do list:

static void main(String[] args) {
   // Create a top-level todo list for the day:
   TaskList todo = new TaskList("To-Do for today:");
   
   // Create a list for the grocery shopping:
   TaskList groceries = new TaskList("Go to the grocery store:");
   groceries.add(new ListItem("Apples"));
   groceries.add(new ListItem("Bread"));
   groceries.add(new ListItem("Yogurt"));
   groceries.add(new ListItem("Popcorn"));
   
   // Add our grocery list to the top-level list:
   todo.add(groceries);
   
   // Create a list for homework:
   TaskList homework = new TaskList("Do my homework:");    
   homework.add(new ListItem("MAT paper"));
   homework.add(new ListItem("PHY quiz"));
   // A sub list for computer science homework:
   TaskList cscHomework = new TaskList("CSC:");
   cscHomework.add(new ListItem("Lab"));
   cscHomework.add(new ListItem("Assignment"));
   cscHomework.add(new ListItem("Make Flashcards"));
   
   // Add our CS homework to the homework list:
   homework.add(cscHomework);
   
   // Add our homework to the top-level list:
   todo.add(homework);
   
   // Read our top-level list:
   todo.read();
    // Done!
 }

> Challenge: implement read() so that it has proper indentation for each sub-list and task!