Composite Design Pattern

The Composite design pattern allows us to arrange objects into tree structures to represent part-whole hierarchies. It allows client to treat individual objects as well as collection of objects in similar manner. For client, the interface to interact with individual objects and composites remains the same, client doesn't require different code for handling individual or collection of objects.

Composite pattern comes under structural design pattern as it provide one of the best ways to arrange similar objects in hierarchical order. A composite object may contains a collection object, where each object itself can be composite. In this tutorial, we will explore the Composite Design Pattern in Java, discussing its structure, implementation, best practices, and advantages.


Structure of the Composite Design Pattern

The Composite Design Pattern consists of the following components:
  • Component : This is the abstract class or interface that declares the common interface for all concrete classes (leaf nodes and composite nodes). It defines operations that can be performed on both individual objects and compositions.

  • Leaf : This is the concrete class that represents individual objects. Leafs implement the operations declared by the component interface.

  • Composite : This is the concrete class that represents compositions of objects. Composites can contain leafs and other composites. They implement the operations declared by the component interface, and they may also provide additional operations related to managing child components.

  • Client : This is the class that interacts with the component, treating both leafs and composites uniformly. The client is unaware of the specific type of objects it is working with, whether they are individual leafs or complex composites.

Advantages of Composite Pattern

  • Uniform Treatment of Objects : The primary advantage of the Composite pattern is its ability to treat both individual objects (leafs) and compositions of objects (composites) uniformly. Clients can work with the entire hierarchy in a consistent manner.

  • Versatility in Constructing Hierarchies : The pattern allows for the creation of diverse and intricate hierarchies by combining leaf nodes and composite nodes, providing flexibility in designing complex structures.

  • Scalability : The system can easily accommodate the addition of new leaf nodes and composite nodes without requiring modifications to existing code. This enhances the system's scalability as it can grow without significant changes.

  • Recursive Operations : Recursive operations can be easily implemented in composites, allowing for efficient traversal of the entire hierarchy. This recursive nature simplifies the implementation of certain operations, such as drawing or processing.

  • Simplified Client Code : Clients interact with the hierarchy using a uniform interface, making the client code more readable and reducing the potential for errors. This abstraction shields clients from the complexity of distinguishing between individual elements and composite structures.

  • Code Reusability : Both leafs and composites can be reused in different contexts. Leaf classes can be reused in various compositions, and composite classes can be nested within other composites, promoting code reusability.

  • Dynamic Composition : The ability to dynamically add or remove components during runtime enhances adaptability. This feature is valuable when the structure of the hierarchy needs to change while the program is running, providing a dynamic and responsive system.
In summary, the Composite design pattern proves advantageous in creating flexible, scalable, and easily maintainable object hierarchies. Its ability to provide a consistent interface, support efficient recursive operations, and enable dynamic composition contributes to the overall robustness and adaptability of software systems employing this pattern.

When we should use Composite Pattern

  • When multiple objects expose the same interface as each of them separately.
  • When client needs a common interface to interact with individual objects and composite structure.
  • When we need to represent the relationship between the objects as a tree.

Implementation of Composite Design Pattern

We will define a Teacher.java class, which will act as a Component of Composite pattern. It contains a list of teachers directly reporting to him.

Composite Design Pattern UML Diagram Teacher.java
import java.util.List;
import java.util.ArrayList;

public class Teacher {
    private String name;
    private String designation;
    private String department;
    private int salary;
    private List<Teacher> directReportees;
 
    public Teacher(String name, String designation, String department, int salary){
        this.name = name;
        this.designation = designation;
        this.department = department;
        this.salary = salary;
        this.directReportees = new ArrayList<Teacher>();
    }
 
    public void add(Teacher t){
        directReportees.add(t);
    }
 
    public void remove(Teacher t){
        directReportees.remove(t);
    }
 
    public void addAll(List<Teacher> list){
        directReportees.addAll(list);
    }
 
    public List<Teacher> getAllReportees(){
        return directReportees;
    }
 
    public void printTeacherDetails(){
        System.out.println("Name : " + name + ", Designation : " +
        designation + ", Department : " + department + ", Salary : " + salary);
    }
 
    public void printAllReportees(){
        printTeacherDetails();
        for(Teacher t : directReportees){
            t.printAllReportees();
        }
    }
}

ComputerScienceDepartment.java creates a heirarchical tree structure of teachers of ComputerScienceDepartment of a college, where all AssistantProfessors report to Professor and all Professors report to HOD. It uses the same Teacher class interface to interact with individual teacher objects as well as whole teacher tree structure.

ComputerScienceDepartment.java
public class ComputerScienceDepartment {
    public static void main(String[] args) {
        Teacher hod = new Teacher("Jack", "HOD", "CSE", 50000);
     
        Teacher professor1 = new Teacher("George", "Professor", "CSE", 40000);
        Teacher professor2 = new Teacher("Mark", "Professor", "CSE", 40000);
     
        Teacher assistantProfessor1 = 
            new Teacher("Emily", "AssistantProfessor", "CSE", 30000);
        Teacher assistantProfessor2 = 
            new Teacher("John", "AssistantProfessor", "CSE", 30000);
        Teacher assistantProfessor3 = 
            new Teacher("Chan", "AssistantProfessor", "CSE", 30000);
        Teacher assistantProfessor4 = 
            new Teacher("Abhijit", "AssistantProfessor", "CSE", 30000);
     
        // Create hierarchy or teachers 
        hod.add(professor1);
        hod.add(professor2);
     
        professor1.add(assistantProfessor1);
        professor1.add(assistantProfessor2);
        professor2.add(assistantProfessor3);
        professor2.add(assistantProfessor4);
     
        // Print List of all Teachers in CSE Department
        hod.printAllReportees();
    }
}

Output

Name : Jack, Designation : HOD, Department : CSE, Salary : 50000
Name : George, Designation : Professor, Department : CSE, Salary : 40000
Name : Emily, Designation : AssistantProfessor, Department : CSE, Salary : 30000
Name : John, Designation : AssistantProfessor, Department : CSE, Salary : 30000
Name : Mark, Designation : Professor, Department : CSE, Salary : 40000
Name : Chan, Designation : AssistantProfessor, Department : CSE, Salary : 30000
Name : Abhijit, Designation : AssistantProfessor, Department : CSE, Salary : 30000

Best Practices of Composite Pattern

  • Use consistent naming conventions for operations across leafs and composites. This consistency improves code readability and makes it easier for developers to understand the purpose of each method.

  • Design a clear and concise component interface that declares common operations for both leafs and composites. The interface should be focused on the essential behaviors applicable to all components.

  • In composite classes, encapsulate the logic for managing child components. This encapsulation helps in maintaining a clear separation of concerns and allows for potential optimizations or changes in child management.

  • Consider making leaf classes immutable if applicable. Immutable leafs simplify their usage and guarantee that their state remains constant, avoiding unexpected changes.

  • Leverage recursion when implementing operations that involve traversing the entire hierarchy of leafs and composites. Recursive operations simplify the code and make it easier to apply the operation uniformly.

  • Ensure that clients are unaware of whether they are working with individual leafs or composites. This lack of awareness allows clients to treat both leafs and composites uniformly, promoting a clean and consistent design.

  • Clearly document the hierarchy of components, indicating which classes act as leafs and which ones act as composites. This documentation helps developers understand how to extend or modify the hierarchy.

  • Consider making leaf classes immutable if applicable. Immutable leafs simplify their usage and guarantee that their state remains constant, avoiding unexpected changes.
Related Topics
Command Design Pattern
State Design Pattern
Strategy Design Pattern
Mediator Design Pattern
Bridge Design Pattern
List of Design Patterns