Composite Pattern

Idea

  • Be able to treat a collection of objects with whole-part relationships uniformly

  • Whole-part relationship means having a top level component, that contains composite objects (component object holding other components) and leaf objects (component object that contain no other components)

  • On any composite and any leaf object the client can call the same methods (i.e. print()) -> the composite objects display by telling their components to call print() and the leaf objects will call print on themselves

  • Each composite or leaf object must implement the same component interface

  • Components have a different method implementation as the leafs

  • Not every method makes sense for every object implementing the component interface -> those can be ignored by returning null or false or throw an exception the client needs to handle (i.e. ignoring by offering an empty catch block)

  • From the client side there is no difference between a composite object or a leaf object within the tree structure

Definition

The Composite Pattern allows you to compute objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual object and compositions of objects uniformly.

        classDiagram
    Component <|-- Leaf : implements
    Component <|-- Composite : implements
    Component "1..*" <-- Composite : ___HAS-A___
    class Component {
        <<interface>>
        +operation()
    }
    class Leaf {
        +operation()
    }
    class Composite {
        +operation()
    }
    

Hint

In the book, the components interface (or abstract class) also features an add(Component) and a remove(Component) method in order to add and remove Components from the hierarchy structure.

This violates the Open-Closed Principle, which does not allow to change the hierarchy structure once it has been created.

It probably depends on the application, if these methods are absolutely required.

But stating these in the Component class will pass these methods onto Leaf classes, which clearly are not supposed to use it (must return None or throw an error, if so). Defining those methods in the Composite class breaks the pattern, since both Composite and Leaf classes should be uniform, so the client mustn’t differentiate (client needed to check the type of the class before using it).

The way a hierarchy should be changed is by creating a modified copy of it (same as when changing the value of a string or integer)

Application

  • Applied to data, which can be hierarchically represented, to compute it easily

  • Often applied in GUIs (main page consists of sub-components, which itself are made up of sub-components, and a refresh() call on an higher-level component will trigger refresh() on all of it’s sub-components and leaves)

Find a template example for Python at https://refactoring.guru/design-patterns/composite/python/example

from abc import ABC, abstractmethod

# Working, but not nice example of a HTML Generator for a To-Do list


# Component
class ToDoList(ABC):

    @abstractmethod
    def get_html(self):
        pass


# Leaf
class ToDoItem(ToDoList):

    def __init__(self, text):
        self.text = text

    def get_html(self):
        return f"<input type=\"checkbox\">{self.text}"


# Composite
class ToDoProject(ToDoList):

    def __init__(self, title, list_of_todos: list):
        self.title = title
        self.list_of_todos = list_of_todos

    def get_html(self):
        html = ""
        if len(self.list_of_todos) > 1:
            html += f"<h1>{self.title}</h1>"
        for item in self.list_of_todos:
            # If item is of type ToDoItem, then we will get the checkbox + text
            # but if it is of type ToDoProject, it will call itself on another
            # instance (-> recursion)
            html += "<li>"
            html += item.get_html()
            html += "</li>"
        return html


if __name__ == "__main__":

    to_do_A = ToDoProject("Do A", [ToDoItem("Doing A")])
    to_do_B = ToDoProject("Do B", [ToDoItem("Doing B")])

    to_do_C1_list = [ToDoItem("Doing C1.A"),
                     ToDoItem("Doing C1.B")]
    to_do_C1 = ToDoProject("Doing C1", to_do_C1_list)

    to_do_C_list = [
                    to_do_C1,
                    ToDoItem("Doing C2"),
                    ToDoItem("Doing C3")]
    to_do_C = ToDoProject("Doing C", to_do_C_list)

    print(to_do_A.get_html())
    print(to_do_B.get_html())
    print(to_do_C.get_html())