Easy patterns in Python: The Composite Pattern

Introduction

The composite pattern allows you treat a group of objects like a single object. The objects are composed into some form of a tree-structure to make this possible.

This patterns solves two problems:

  • Sometimes it is practical to treat part and whole objects the same way
  • An object hierarchy should be represented as a tree structure

We do this by doing the following:

  • Define a unified interface for both the part objects (Leaf) and the whole object (Composite)
  • Composite delegate calls to that interface to their children, Leaf objects deal with them directly.

This all sounds rather cryptic, so let us have a look at the diagram:

This is basically a graphical representation of the last two points: Composites delegate and Leafs perform the actual operation.

Implementation in Python

In this example we will deal with a country, and provinces.

We will start by defining GeographicalEntity with just one method:

class GeographicalEntity:
    def search(self, term: str):
        pass

Next we define the Country class:

class Country(GeographicalEntity):
    _name: str = None
    _provinces: list[GeographicalEntity] = None

    def __init__(self, name: str):
        self._name = name
        self._provinces = []

    def search(self, term: str):
        print(f"Search for city {term} in country {self._name}")
        for composite in self._provinces:
            composite.search(term)

    def add_province(self, new_province: GeographicalEntity):
        self._provinces.append(new_province)

Some notes:

  1. A country consists of some provinces. That is where the _provinces variable comes from
  2. A country also has a name
  3. In the search we iterate over the _provinces variable. Since the objects implement the search() method, we delegate our search-request to them. Note that any object that satisfies the GeographicalEntity interface could be in that array.
  4. The addProvince() simply adds a province, or to be precise an object satisfying the GeographicalEntity interface, to the _provinces list.

Next we implement the Province:

class Province(GeographicalEntity):
    _name: str = None

    def __init__(self, name: str):
        self._name = name

    def search(self, term: str):
        print(f"Search for city {term} in province {self._name}")

Also some notes here:

  1. The Province struct is the Leaf node of our current setup.
  2. Province has a name.
  3. In the search() method we simply announce we are searching

Time to test

Time to test our little database:

if __name__ == "__main__":
    province1: GeographicalEntity = Province("province1")
    province2: GeographicalEntity = Province("province2")

    country = Country("country")

    country.add_province(province1)
    country.add_province(province2)

    country.search("city")

Line by line:

  1. We construct two provinces, and one country
  2. We add the provinces to the country
  3. And the we search for the city ‘city’

Conclusion

This pattern is one of the most versatile patterns. I have used it here to model a country with provinces. Some of the more canonical examples use a file-system with files and folders, and there are probably dozens other use-cases.

One possible enhancement would be to make the search multi-threaded, so that it can be done more efficiently. In our use-case that could work since the searches are independent of each other.

Leave a Reply

Your email address will not be published. Required fields are marked *