7.1. Composite

  • EN: Composite

  • PL: Kompozyt

  • Type: object

The Composite design pattern is a structural design pattern that allows you to compose objects into a tree structure and work with these objects as if they were individual objects. This pattern is particularly useful when dealing with a hierarchy of objects.

Here's a simple example of the Composite pattern in Python:

>>> from abc import ABC, abstractmethod
...
>>> class Component(ABC):
...     @abstractmethod
...     def operation(self) -> str:
...         pass
...
>>> class Leaf(Component):
...     def operation(self) -> str:
...         return "Leaf"
...
>>> class Composite(Component):
...     def __init__(self) -> None:
...         self._children = []
...
...     def add(self, component: Component) -> None:
...         self._children.append(component)
...
...     def remove(self, component: Component) -> None:
...         self._children.remove(component)
...
...     def operation(self) -> str:
...         results = []
...         for child in self._children:
...             results.append(child.operation())
...         return f"Branch({'+'.join(results)})"
...
>>> simple = Leaf()
>>> print(simple.operation())
Leaf
>>> tree = Composite()
>>> tree.add(simple)
>>> branch = Composite()
>>> branch.add(Leaf())
>>> branch.add(Leaf())
>>> tree.add(branch)
>>> print(tree.operation())
Branch(Leaf+Branch(Leaf+Leaf))

In this example, Component is an abstract base class that declares an interface for all components, including both simple, leaf components and complex, composite ones. Leaf is a class that represents simple components that perform some operation. Composite is a class that represents complex components that can have other components (both Leaf and Composite) as children. The Composite class overrides methods to add or remove children and to perform an operation. The operation method in the Composite class performs the operation on each child and sums up the results.

7.1.1. Pattern

  • Represent a hierarchy of objects

  • Groups (and subgroups) objects in Keynote

  • Files in a Folder; when you move folder you also move files

  • allows you to represent individual entities and groups of entities in the same manner.

  • is a structural design pattern that lets you compose objects into a tree.

  • is great if you need the option of swapping hierarchical relationships around.

  • makes it easier for you to add new kinds of components

  • conform to the Single Responsibility Principle in the way that it separates the aggregation of objects from the features of the object.

../../_images/designpatterns-composite-pattern.png

7.1.2. Problem

design-patterns/structural/img/designpatterns-composite-problem.png

from typing import Self
from dataclasses import dataclass, field


@dataclass
class Shape:
    name: str

    def render(self) -> None:
        print(f'Render {self.name}')

    def move(self) -> None:
        print(f'Move {self.name}')


@dataclass
class Group:
    objects: list[Shape | Self] = field(default_factory=list)

    def add(self, obj: Shape | Self) -> None:
        self.objects.append(obj)

    def render(self) -> None:
        for obj in self.objects:
            obj.render()

    def move(self) -> None:
        for obj in self.objects:
            obj.move()


if __name__ == '__main__':
    group1 = Group()
    group1.add(Shape('square'))
    group1.add(Shape('rectangle'))

    group2 = Group()
    group2.add(Shape('circle'))
    group2.add(Shape('ellipse'))

    everything = Group()
    everything.add(group1)
    everything.add(group2)

    everything.render()
    # Render square
    # Render rectangle
    # Render circle
    # Render ellipse

    everything.move()
    # Move square
    # Move rectangle
    # Move circle
    # Move ellipse

7.1.3. Solution

../../_images/designpatterns-composite-solution.png

from abc import ABC, abstractmethod
from dataclasses import dataclass, field


class Component(ABC):
    @abstractmethod
    def render(self) -> None:
        pass

    @abstractmethod
    def move(self) -> None:
        pass


@dataclass
class Shape(Component):
    name: str

    def move(self) -> None:
        print(f'Move {self.name}')

    def render(self) -> None:
        print(f'Render {self.name}')


@dataclass
class Group(Component):
    components: list[Component] = field(default_factory=list)

    def add(self, component: Component) -> None:
        self.components.append(component)

    def render(self) -> None:
        for component in self.components:
            component.render()

    def move(self) -> None:
        for component in self.components:
            component.move()


if __name__ == '__main__':
    group1 = Group()
    group1.add(Shape('square'))
    group1.add(Shape('rectangle'))

    group2 = Group()
    group2.add(Shape('circle'))
    group2.add(Shape('ellipse'))

    everything = Group()
    everything.add(group1)
    everything.add(group2)

    everything.render()
    # Render square
    # Render rectangle
    # Render circle
    # Render ellipse

    everything.move()
    # Move square
    # Move rectangle
    # Move circle
    # Move ellipse

7.1.4. Assignments

from abc import ABC, abstractmethod
from dataclasses import dataclass, field


class Shape(ABC):
    @abstractmethod
    def print(self):
        pass


class Ellipse(Shape):
    def print(self):
        print('Ellipse')


class Circle(Shape):
    def print(self):
        print('Circle')


@dataclass
class Group(Shape):
    children: list = field(default_factory=list)

    def add(self, graphic):
        self.children.append(graphic)

    def print(self):
        for children in self.children:
            children.print()


if __name__ == '__main__':
    group1 = Group()
    group1.add(Ellipse())

    group2 = Group()
    group2.add(Circle())
    group2.add(group1)
    group2.print()