Composite

Паттерн для древовидных структур: с отдельным объектом и группой объектов работаем одинаково.

Composite компонует объекты в древовидную структуру и позволяет клиенту работать с одиночными объектами и их группами через общий интерфейс.

Какую задачу решает

Папка содержит файлы и другие папки. Меню содержит пункты и подменю. У таких структур есть «листья» и «контейнеры», и хочется обращаться с ними единообразно: попросить размер у файла и у целой папки одинаковым вызовом. Composite даёт обоим общий интерфейс, а контейнер рекурсивно опрашивает детей.

Идея и реализация

from abc import ABC, abstractmethod


class Node(ABC):
    @abstractmethod
    def size(self): ...


class File(Node):                # лист
    def __init__(self, name, size):
        self.name = name
        self._size = size

    def size(self):
        return self._size


class Folder(Node):              # контейнер
    def __init__(self, name):
        self.name = name
        self.children = []

    def add(self, node):
        self.children.append(node)
        return self

    def size(self):
        return sum(child.size() for child in self.children)


root = Folder("root")
root.add(File("a.txt", 100)).add(File("b.txt", 250))
sub = Folder("sub")
sub.add(File("c.txt", 50))
root.add(sub)

print("Размер root:", root.size())

Вывод:

Размер root: 400

Метод size() у файла возвращает своё число, а у папки — рекурсивно суммирует детей, не зная, файлы это или вложенные папки. Клиент вызывает root.size() и получает 400 (100 + 250 + 50). В этом сила паттерна: рекурсия скрыта, интерфейс общий.

Обратите внимание, что добавление новой операции (например, count_files() или render()) сводится к одному методу в общем интерфейсе, который лист и контейнер реализуют по-своему. А любая операция над всем деревом пишется без единого isinstance: вы просто вызываете её на корне, и она «протекает» вниз по структуре. Именно отказ от проверок типа делает код с Composite чистым и расширяемым.

Тонкий вопрос проектирования — где объявлять метод add. Положить его в общий интерфейс удобно (клиент работает единообразно), но тогда лист вынужден как-то реагировать на бессмысленный для него вызов. Альтернатива — держать add только в контейнере; тогда клиент иногда должен различать узлы. Это классический компромисс Composite между «прозрачностью» и «безопасностью»; единственно верного ответа нет, выбирайте по контексту.

Плюсы и минусы

  • Плюс: единообразная работа с деревом любой глубины; новые типы узлов добавляются легко.
  • Минус: общий интерфейс может стать слишком общим (метод add бессмыслен для листа).

Где встречается

Файловые системы, DOM-дерево HTML, дерево UI-компонентов (виджет содержит виджеты), AST в компиляторах, графические редакторы (группа фигур ведёт себя как фигура).

Итог

  • Composite — для рекурсивных древовидных структур.
  • Лист и контейнер реализуют общий интерфейс; контейнер делегирует детям.
  • Клиент не различает одиночный объект и группу.
Проверьте себя
1. Для каких структур предназначен Composite?
AЛинейных списков
BДревовидных (иерархических) структур
CХеш-таблиц
DОчередей
2. Как контейнер обычно вычисляет свой результат?
AХранит готовое значение
BРекурсивно опрашивает своих детей через общий интерфейс
CЗапрашивает у синглтона
DСлучайным образом
3. Какой минус у Composite?
AОн не поддерживает рекурсию
BОбщий интерфейс может содержать методы, бессмысленные для листьев
CОн требует синглтона
DОн работает только с числами
Поддержать проект