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:
- A country consists of some provinces. That is where the _provinces variable comes from
- A country also has a name
- 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.
- 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:
- The Province struct is the Leaf node of our current setup.
- A Province has a name.
- 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:
- We construct two provinces, and one country
- We add the provinces to the country
- 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.