Перейти к содержанию

Компоновщик

Также известен как: Дерево, Composite

Компоновщик — это структурный паттерн проектирования, который позволяет сгруппировать множество объектов в древовидную структуру, а затем работать с ней так, как будто это единичный объект.

Паттерн Компоновщик

Проблема

Паттерн Компоновщик имеет смысл только тогда, когда основная модель вашей программы может быть структурирована в виде дерева.

Например, есть два объекта: Продукт и Коробка. Коробка может содержать несколько Продуктов и других Коробок поменьше. Те, в свою очередь, тоже содержат либо Продукты, либо Коробки и так далее.

Теперь предположим, ваши Продукты и Коробки могут быть частью заказов. Каждый заказ может содержать как простые Продукты без упаковки, так и составные Коробки. Ваша задача состоит в том, чтобы узнать цену всего заказа.

Структура сложного заказа

Заказ может состоять из различных продуктов, упакованных в собственные коробки.

Если решать задачу в лоб, то вам потребуется открыть все коробки заказа, перебрать все продукты и посчитать их суммарную стоимость. Но это слишком хлопотно, так как типы коробок и их содержимое могут быть вам неизвестны. Кроме того, наперёд неизвестно и количество уровней вложенности коробок, поэтому перебрать коробки простым циклом не выйдет.

Решение

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

Продукт просто вернёт свою цену. Коробка спросит цену каждого предмета внутри себя и вернёт сумму результатов. Если одним из внутренних предметов окажется коробка поменьше, она тоже будет перебирать своё содержимое, и так далее, пока не будут посчитаны все составные части.

Решение с Компоновщиком

Компоновщик рекурсивно запускает действие по всем элементам дерева — от корня к листьям.

Для вас, клиента, главное, что теперь не нужно ничего знать о структуре заказов. Вы вызываете метод получения цены, он возвращает цифру, а вы не тонете в горах картона и скотча.

Аналогия из жизни

Пример армейской структуры

Пример армейской структуры.

Армии большинства государств могут быть представлены в виде перевёрнутых деревьев. На нижнем уровне у вас есть солдаты, затем взводы, затем полки, а затем целые армии. Приказы отдаются сверху и спускаются вниз по структуре командования, пока не доходят до конкретного солдата.

Структура

Структура классов паттерна Компоновщик

  1. Компонент определяет общий интерфейс для простых и составных компонентов дерева.
  2. Лист — это простой компонент дерева, не имеющий ответвлений.
    Из-за того, что им некому больше передавать выполнение, классы листьев будут содержать большую часть полезного кода.
  3. Контейнер (или композит) — это составной компонент дерева. Он содержит набор дочерних компонентов, но ничего не знает об их типах. Это могут быть как простые компоненты-листья, так и другие компоненты-контейнеры. Но это не является проблемой, если все дочерние компоненты следуют единому интерфейсу.
    Методы контейнера переадресуют основную работу своим дочерним компонентам, хотя и могут добавлять что-то своё к результату.
  4. Клиент работает с деревом через общий интерфейс компонентов.

Благодаря этому, клиенту не важно, что перед ним находится — простой или составной компонент дерева.

Псевдокод

В этом примере Компоновщик помогает реализовать вложенные геометрические фигуры.

Структура классов примера паттерна Компоновщик

Пример редактора геометрических фигур.

Класс CompoundGraphic может содержать любое количество подфигур, включая такие же контейнеры, как он сам. Контейнер реализует те же методы, что и простые фигуры. Но, вместо непосредственного действия, он передаёт вызовы всем вложенным компонентам, используя рекурсию. Затем он как бы «суммирует» результаты всех вложенных фигур.

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

// Общий интерфейс компонентов.
interface Graphic is
    method move(x, y)
    method draw()

// Простой компонент.
class Dot implements Graphic is
    field x, y

    constructor Dot(x, y) { ... }

    method move(x, y) is
        this.x += x, this.y += y

    method draw() is
        // Нарисовать точку в координате X, Y.

// Компоненты могут расширять другие компоненты.
class Circle extends Dot is
    field radius

    constructor Circle(x, y, radius) { ... }

    method draw() is
        // Нарисовать окружность в координате X, Y и радиусом R.

// Контейнер содержит операции добавления/удаления дочерних
// компонентов. Все стандартные операции интерфейса компонентов
// он делегирует каждому из дочерних компонентов.
class CompoundGraphic implements Graphic is
    field children: array of Graphic

    method add(child: Graphic) is
        // Добавить компонент в список дочерних.

    method remove(child: Graphic) is
        // Убрать компонент из списка дочерних.

    method move(x, y) is
        foreach (child in children) do
            child.move(x, y)

    method draw() is
        // 1. Для каждого дочернего компонента:
        //     - Отрисовать компонент.
        //     - Определить координаты максимальной границы.
        // 2. Нарисовать пунктирную границу вокруг всей области.


// Приложение работает единообразно как с единичными
// компонентами, так и с целыми группами компонентов.
class ImageEditor is
    field all: CompoundGraphic

    method load() is
        all = new CompoundGraphic()
        all.add(new Dot(1, 2))
        all.add(new Circle(5, 3, 10))
        // ...

    // Группировка выбранных компонентов в один сложный
    // компонент.
    method groupSelected(components: array of Graphic) is
        group = new CompoundGraphic()
        foreach (component in components) do
            group.add(component)
            all.remove(component)
        all.add(group)
        // Все компоненты будут отрисованы.
        all.draw()

Применимость

Когда вам нужно представить древовидную структуру объектов.

Паттерн Компоновщик предлагает хранить в составных объектах ссылки на другие простые или составные объекты. Те, в свою очередь, тоже могут хранить свои вложенные объекты и так далее. В итоге вы можете строить сложную древовидную структуру данных, используя всего две основные разновидности объектов.

Когда клиенты должны единообразно трактовать простые и составные объекты.

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

Шаги реализации

  1. Убедитесь, что вашу бизнес-логику можно представить как древовидную структуру. Попытайтесь разбить её на простые компоненты и контейнеры. Помните, что контейнеры могут содержать как простые компоненты, так и другие вложенные контейнеры.
  2. Создайте общий интерфейс компонентов, который объединит операции контейнеров и простых компонентов дерева. Интерфейс будет удачным, если вы сможете использовать его, чтобы взаимозаменять простые и составные компоненты без потери смысла.
  3. Создайте класс компонентов-листьев, не имеющих дальнейших ответвлений. Имейте в виду, что программа может содержать несколько таких классов.
  4. Создайте класс компонентов-контейнеров и добавьте в него массив для хранения ссылок на вложенные компоненты. Этот массив должен быть способен содержать как простые, так и составные компоненты, поэтому убедитесь, что он объявлен с типом интерфейса компонентов. Реализуйте в контейнере методы интерфейса компонентов, помня о том, что контейнеры должны делегировать основную работу своим дочерним компонентам.
  5. Добавьте операции добавления и удаления дочерних компонентов в класс контейнеров.
    Имейте в виду, что методы добавления/удаления дочерних компонентов можно поместить и в интерфейс компонентов. Да, это нарушит принцип разделения интерфейса, так как реализации методов будут пустыми в компонентах-листьях. Но зато все компоненты дерева станут действительно одинаковыми для клиента.

Преимущества и недостатки

Преимущества

  • Упрощает архитектуру клиента при работе со сложным деревом компонентов.
  • Облегчает добавление новых видов компонентов.

Недостатки

  • Создаёт слишком общий дизайн классов.

Примеры реализации паттерна

Python

  • Сложность: 2/3
  • Популярность: 1/3
  • Применимость: Паттерн Компоновщик встречается в любых задачах, которые связаны с построением дерева. Самый простой пример — составные элементы GUI, которые тоже можно рассматривать как дерево.
  • Признаки применения паттерна: Если из объектов строится древовидная структура, и со всеми объектами дерева, как и с самим деревом работают через общий интерфейс.

Этот пример показывает структуру паттерна Компоновщик, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.

from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List


class Component(ABC):
    """
    Базовый класс Компонент объявляет общие операции как для простых,
    так и для сложных объектов структуры.
    """

    @property
    def parent(self) -> Component:
        return self._parent

    @parent.setter
    def parent(self, parent: Component):
        """
        При необходимости базовый Компонент может объявить интерфейс
        для установки и получения родителя компонента в древовидной
        структуре. Он также может предоставить некоторую реализацию по
        умолчанию для этих методов.
        """

        self._parent = parent

    """
    В некоторых случаях целесообразно определить операции управления
    потомками прямо в базовом классе Компонент. Таким образом, вам не
    нужно будет предоставлять конкретные классы компонентов
    клиентскому коду, даже во время сборки дерева объектов. Недостаток
    такого подхода в том, что эти методы будут пустыми для компонентов
    уровня листа.
    """

    def add(self, component: Component) -> None:
        pass

    def remove(self, component: Component) -> None:
        pass

    def is_composite(self) -> bool:
        """
        Вы можете предоставить метод, который позволит клиентскому
        коду понять, может ли компонент иметь вложенные объекты.
        """

        return False

    @abstractmethod
    def operation(self) -> str:
        """
        Базовый Компонент может сам реализовать некоторое поведение по
        умолчанию или поручить это конкретным классам, объявив метод,
        содержащий поведение абстрактным.
        """

        pass


class Leaf(Component):
    """
    Класс Лист представляет собой конечные объекты структуры. Лист не
    может иметь вложенных компонентов.

    Обычно объекты Листьев выполняют фактическую работу, тогда как
    объекты Контейнера лишь делегируют работу своим подкомпонентам.
    """

    def operation(self) -> str:
        return "Leaf"


class Composite(Component):
    """
    Класс Контейнер содержит сложные компоненты, которые могут иметь
    вложенные компоненты. Обычно объекты Контейнеры делегируют
    фактическую работу своим детям, а затем «суммируют» результат.
    """

    def __init__(self) -> None:
        self._children: List[Component] = []

    """
    Объект контейнера может как добавлять компоненты в свой список 
    вложенных компонентов, так и удалять их, как простые, так и
    сложные.
    """

    def add(self, component: Component) -> None:
        self._children.append(component)
        component.parent = self

    def remove(self, component: Component) -> None:
        self._children.remove(component)
        component.parent = None

    def is_composite(self) -> bool:
        return True

    def operation(self) -> str:
        """
        Контейнер выполняет свою основную логику особым образом. Он
        проходит рекурсивно через всех своих детей, собирая и суммируя
        их результаты.
        Поскольку потомки контейнера передают эти вызовы своим
        потомкам и так далее, в результате обходится всё дерево
        объектов.
        """

        results = []
        for child in self._children:
            results.append(child.operation())
        return f"Branch({'+'.join(results)})"


def client_code(component: Component) -> None:
    """
    Клиентский код работает со всеми компонентами через базовый
    интерфейс.
    """

    print(f"RESULT: {component.operation()}", end="")


def client_code2(component1: Component,
  component2: Component) -> None:
    """
    Благодаря тому, что операции управления потомками объявлены в
    базовом классе Компонента, клиентский код может работать как с
    простыми, так и со сложными компонентами, вне зависимости от их
    конкретных классов.
    """

    if component1.is_composite():
        component1.add(component2)

    print(f"RESULT: {component1.operation()}", end="")


if __name__ == "__main__":
    # Таким образом, клиентский код может поддерживать простые
    # компоненты-листья...
    simple = Leaf()
    print("Client: I've got a simple component:")
    client_code(simple)
    print("\n")

    # ...а также сложные контейнеры.
    tree = Composite()

    branch1 = Composite()
    branch1.add(Leaf())
    branch1.add(Leaf())

    branch2 = Composite()
    branch2.add(Leaf())

    tree.add(branch1)
    tree.add(branch2)

    print("Client: Now I've got a composite tree:")
    client_code(tree)
    print("\n")

    print("Client: I don't need to check the components classes even "
      + "when managing the tree:")
    client_code2(tree, simple)
Client: I've got a simple component:
RESULT: Leaf

Client: Now I've got a composite tree:
RESULT: Branch(Branch(Leaf+Leaf)+Branch(Leaf))

Client: I don't need to check the components classes even when
managing the tree:
RESULT: Branch(Branch(Leaf+Leaf)+Branch(Leaf)+Leaf)

PHP

  • Сложность: 2/3
  • Популярность: 2/3
  • Применимость: Паттерн Компоновщик встречается в любых задачах, которые связаны с построением дерева. Самый простой пример — составные элементы DOM-дерева, которые тоже можно рассматривать как поддерево.
  • Признаки применения паттерна: Если из объектов строится древовидная структура, и со всеми объектами дерева, как и с самим деревом работают через общий интерфейс.

Этот пример показывает структуру паттерна Компоновщик, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.

После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.

<?php
/**
* Базовый класс Компонент объявляет общие операции как для простых, так и для
* сложных объектов структуры.
*/

abstract class Component
{
    /**
    * @var Component|null
    */
    protected $parent;

    /**
    * При необходимости базовый Компонент может объявить интерфейс для
    * установки и получения родителя компонента в древовидной структуре. Он
    * также может предоставить некоторую реализацию по умолчанию для этих
    * методов.
    */
    public function setParent(?Component $parent)
    {
        $this->parent = $parent;
    }

    public function getParent(): Component
    {
        return $this->parent;
    }

    /**
    * В некоторых случаях целесообразно определить операции управления
    * потомками прямо в базовом классе Компонент. Таким образом, вам не нужно
    * будет предоставлять конкретные классы компонентов клиентскому коду, даже
    * во время сборки дерева объектов. Недостаток такого подхода в том, что эти
    * методы будут пустыми для компонентов уровня листа.
    */
    public function add(Component $component): void { }

    public function remove(Component $component): void { }

    /**
    * Вы можете предоставить метод, который позволит клиентскому коду понять,
    * может ли компонент иметь вложенные объекты.
    */
    public function isComposite(): bool
    {
        return false;
    }

    /**
    * Базовый Компонент может сам реализовать некоторое поведение по умолчанию
    * или поручить это конкретным классам, объявив метод, содержащий поведение
    * абстрактным.
    */
    abstract public function operation(): string;
}

/**
* Класс Лист представляет собой конечные объекты структуры. Лист не может иметь
* вложенных компонентов.
*
* Обычно объекты Листьев выполняют фактическую работу, тогда как объекты
* Контейнера лишь делегируют работу своим подкомпонентам.
*/

class Leaf extends Component
{
    public function operation(): string
    {
        return "Leaf";
    }
}

/**
* Класс Контейнер содержит сложные компоненты, которые могут иметь вложенные
* компоненты. Обычно объекты Контейнеры делегируют фактическую работу своим
* детям, а затем «суммируют» результат.
*/

class Composite extends Component
{
    /**
    * @var \SplObjectStorage
    */
    protected $children;

    public function __construct()
    {
        $this->children = new \SplObjectStorage();
    }

    /**
    * Объект контейнера может как добавлять компоненты в свой список вложенных
    * компонентов, так и удалять их, как простые, так и сложные.
    */
    public function add(Component $component): void
    {
        $this->children->attach($component);
        $component->setParent($this);
    }

    public function remove(Component $component): void
    {
        $this->children->detach($component);
        $component->setParent(null);
    }

    public function isComposite(): bool
    {
        return true;
    }

    /**
    * Контейнер выполняет свою основную логику особым образом. Он проходит
    * рекурсивно через всех своих детей, собирая и суммируя их результаты.
    * Поскольку потомки контейнера передают эти вызовы своим потомкам и так
    * далее, в результате обходится всё дерево объектов.
    */
    public function operation(): string
    {
        $results = [];
        foreach ($this->children as $child) {
            $results[] = $child->operation();
        }
        return "Branch(" . implode("+", $results) . ")";
    }
}

/**
* Клиентский код работает со всеми компонентами через базовый интерфейс.
*/

function clientCode(Component $component)
{
    // ...
    echo "RESULT: " . $component->operation();
    // ...
}

/**
* Таким образом, клиентский код может поддерживать простые компоненты-листья...
*/
$simple = new Leaf();
echo "Client: I've got a simple component:\n";
clientCode($simple);
echo "\n\n";

/**
* ...а также сложные контейнеры.
*/
$tree = new Composite();
$branch1 = new Composite();
$branch1->add(new Leaf());
$branch1->add(new Leaf());
$branch2 = new Composite();
$branch2->add(new Leaf());
$tree->add($branch1);
$tree->add($branch2);
echo "Client: Now I've got a composite tree:\n";
clientCode($tree);
echo "\n\n";

/**
* Благодаря тому, что операции управления потомками объявлены в базовом классе
* Компонента, клиентский код может работать как с простыми, так и со сложными
* компонентами, вне зависимости от их конкретных классов.
*/

function clientCode2(Component $component1, Component $component2)
{
    // ...
    if ($component1->isComposite()) {
        $component1->add($component2);
    }
    echo "RESULT: " . $component1->operation();
    // ...
}

echo "Client: I don't need to check the components classes even when managing the tree:\n";
clientCode2($tree, $simple);
Client: I get a simple component:
RESULT: Leaf

Client: Now I get a composite tree:
RESULT: Branch(Branch(Leaf+Leaf)+Branch(Leaf))

Client: I don't need to check the components classes even when managing the tree::
RESULT: Branch(Branch(Leaf+Leaf)+Branch(Leaf)+Leaf)

Паттерн Компоновщик может упростить работу с любыми древовидными рекурсивными структурами. Примером такой структуры является DOM-дерево HTML. Например, в то время как различные входные элементы могут служить листьями, сложные элементы, такие как формы и наборы полей, играют роль контейнеров.

Имея это в виду, вы можете использовать паттерн Компоновщик для применения различных типов поведения ко всему дереву HTML точно так же, как и к его внутренним элементам, не привязывая ваш код к конкретным классам дерева DOM. Примерами такого поведения может быть рендеринг элементов DOM, их экспорт в различные форматы, проверка достоверности их частей и т. д.

С паттерном Компоновщик вам не нужно проверять, является ли тип элемента простым или сложным, перед реализацией поведения. В зависимости от типа элемента, оно либо сразу же выполняется, либо передаётся всем дочерним элементам.

<?php
/**
* Базовый класс Компонент объявляет интерфейс для всех конкретных компонентов,
* как простых, так и сложных.
*
* В нашем примере мы сосредоточимся на поведении рендеринга элементов DOM.
*/

abstract class FormElement
{
    /**
    * Можно предположить, что всем элементам DOM будут нужны эти 3 поля.
    */
    protected $name;
    protected $title;
    protected $data;

    public function __construct(string $name, string $title)
    {
        $this->name = $name;
        $this->title = $title;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function setData($data): void
    {
        $this->data = $data;
    }

    public function getData(): array
    {
        return $this->data;
    }

    /**
    * Каждый конкретный элемент DOM должен предоставить свою реализацию
    * рендеринга, но мы можем с уверенностью предположить, что все они будут
    * возвращать строки.
    */
    abstract public function render(): string;
}

/**
* Это компонент-Лист. Как и все Листья, он не может иметь вложенных
* компонентов.
*/

class Input extends FormElement
{
    private $type;

    public function __construct(string $name, string $title, string $type)
    {
        parent::__construct($name, $title);
        $this->type = $type;
    }

    /**
    * Поскольку у компонентов-Листьев нет вложенных компонентов, которые могут
    * выполнять за них основную часть работы, обычно Листья делают большую
    * часть тяжёлой работы внутри паттерна Компоновщик.
    */
    public function render(): string
    {
        return "<label for=\"{$this->name}\">{$this->title}</label>\n" .
            "<input name=\"{$this->name}\" type=\"{$this->type}\" value=\"{$this->data}\">\n";
    }
}

/**
* Базовый класс Контейнер реализует инфраструктуру для управления дочерними
* объектами, повторно используемую всеми Конкретными Контейнерами.
*/ 

abstract class FieldComposite extends FormElement
{
    /**
    * @var FormElement[]
    */
    protected $fields = [];

    /**
    * Методы добавления/удаления подобъектов.
    */
    public function add(FormElement $field): void
    {
        $name = $field->getName();
        $this->fields[$name] = $field;
    }

    public function remove(FormElement $component): void
    {
        $this->fields = array_filter($this->fields, function ($child) use ($component) {
            return $child != $component;
        });
    }

    /**
    * В то время как метод Листа просто выполняет эту работу, метод Контейнера
    * почти всегда должен учитывать его подобъекты.
    *
    * В этом случае контейнер может принимать структурированные данные.
    *
    * @param array $data
    */
    public function setData($data): void
    {
        foreach ($this->fields as $name => $field) {
            if (isset($data[$name])) {
                $field->setData($data[$name]);
            }
        }
    }

    /**
    * Та же логика применима и к получателю. Он возвращает структурированные
    * данные самого контейнера (если они есть), а также все дочерние данные.
    */
    public function getData(): array
    {
        $data = [];
        foreach ($this->fields as $name => $field) {
            $data[$name] = $field->getData();
        }
        return $data;
    }

    /**
    * Базовая реализация рендеринга Контейнера просто объединяет результаты
    * всех дочерних элементов. Конкретные Контейнеры смогут повторно
    * использовать эту реализацию в своих реальных реализациях рендеринга.
    */
    public function render(): string
    {
        $output = "";
        foreach ($this->fields as $name => $field) {
            $output .= $field->render();
        }
        return $output;
    }
}

/**
* Элемент fieldset представляет собой Конкретный Контейнер.
*/ 

class Fieldset extends FieldComposite
{
    public function render(): string
    {
        // Обратите внимание, как комбинированный результат рендеринга потомков
        // включается в тег fieldset.
        $output = parent::render();
        return "<fieldset><legend>{$this->title}</legend>\n$output</fieldset>\n";
    }
}

/**
* Так же как и элемент формы.
*/

class Form extends FieldComposite
{
    protected $url;

    public function __construct(string $name, string $title, string $url)
    {
        parent::__construct($name, $title);
        $this->url = $url;
    }

    public function render(): string
    {
        $output = parent::render();
        return "<form action=\"{$this->url}\">\n<h3>{$this->title}</h3>\n$output</form>\n";
    }
}

/**
* Клиентский код получает удобный интерфейс для построения сложных древовидных
* структур.
*/

function getProductForm(): FormElement
{
    $form = new Form('product', "Add product", "/product/add");
    $form->add(new Input('name', "Name", 'text'));
    $form->add(new Input('description', "Description", 'text'));

    $picture = new Fieldset('photo', "Product photo");
    $picture->add(new Input('caption', "Caption", 'text'));
    $picture->add(new Input('image', "Image", 'file'));
    $form->add($picture);

    return $form;
}

/**
* Структура формы может быть заполнена данными из разных источников. Клиент не
* должен проходить через все поля формы, чтобы назначить данные различным
* полям, так как форма сама может справиться с этим.
*/

function loadProductData(FormElement $form)
{
    $data = [
        'name' => 'Apple MacBook',
        'description' => 'A decent laptop.',
        'photo' => [
            'caption' => 'Front photo.',
            'image' => 'photo1.png',
        ],
    ];
    $form->setData($data);
}

/**
* Клиентский код может работать с элементами формы, используя абстрактный
* интерфейс. Таким образом, не имеет значения, работает ли клиент с простым
* компонентом или сложным составным деревом.
*/

function renderProduct(FormElement $form)
{
    // ..
    echo $form->render();
    // ..
}

$form = getProductForm();
loadProductData($form);
renderProduct($form);
<form action="/product/add">
    <h3>Add product</h3>
    <label for="name">Name</label>
    <input name="name" type="text" value="Apple MacBook">
    <label for="description">Description</label>
    <input name="description" type="text" value="A decent laptop.">
    <fieldset>
        <legend>Product photo</legend>
        <label for="caption">Caption</label>
        <input name="caption" type="text" value="Front photo.">
        <label for="image">Image</label>
        <input name="image" type="file" value="photo1.png">
    </fieldset>
</form>

Задачи

Задача 1. Файловая система

Реализуйте паттерн Компоновщик для файловой системы. Листьевой элемент — File (имеет размер). Контейнер — Directory (содержит файлы и другие директории). Оба реализуют интерфейс FileSystemNode с методами get_size(), print_tree(indent) и search(name). Покажите, что вызов get_size() на директории рекурсивно суммирует размеры всех вложенных файлов.

from abc import ABC, abstractmethod

class FileSystemNode(ABC):
    @abstractmethod
    def get_size(self) -> int: pass

    @abstractmethod
    def print_tree(self, indent: int = 0) -> None: pass

    @abstractmethod
    def search(self, name: str) -> list['FileSystemNode']: pass

# TODO: реализовать File(FileSystemNode):
# - __init__(name: str, size: int)
# - get_size() -> возвращает size
# - print_tree(indent) -> печатает "  " * indent + "📄 name (size bytes)"
# - search(name) -> [self] если self.name == name, иначе []

# TODO: реализовать Directory(FileSystemNode):
# - __init__(name: str)
# - add(node: FileSystemNode), remove(node: FileSystemNode)
# - get_size() -> сумма размеров всех дочерних узлов
# - print_tree(indent) -> "  " * indent + "📁 name/" и рекурсия по детям
# - search(name) -> собирает совпадения по всему дереву

# Ожидаемый клиентский код:
# root = Directory("project")
# src = Directory("src")
# src.add(File("main.py", 1200))
# src.add(File("utils.py", 800))
# tests = Directory("tests")
# tests.add(File("test_main.py", 600))
# root.add(src)
# root.add(tests)
# root.add(File("README.md", 300))

# root.print_tree()
# print(f"Общий размер: {root.get_size()} байт")
# print("Поиск 'utils.py':", root.search("utils.py"))
<?php
interface FileSystemNode {
    public function getSize(): int;
    public function printTree(int $indent = 0): void;
    public function search(string $name): array;
}

// TODO: реализовать File implements FileSystemNode:
// - __construct(string $name, int $size)
// - getSize() -> возвращает $size
// - printTree($indent) -> выводит str_repeat('  ', $indent) . "📄 name (size bytes)"
// - search($name) -> [$this] если $this->name === $name, иначе []

// TODO: реализовать Directory implements FileSystemNode:
// - __construct(string $name)
// - add(FileSystemNode $node), remove(FileSystemNode $node)
// - getSize() -> сумма размеров всех дочерних узлов
// - printTree($indent) -> str_repeat('  ', $indent) . "📁 name/" и рекурсия по детям
// - search($name) -> собирает совпадения по всему дереву

// Ожидаемый клиентский код:
// $root = new Directory('project');
// $src = new Directory('src');
// $src->add(new File('main.py', 1200));
// $src->add(new File('utils.py', 800));
// $tests = new Directory('tests');
// $tests->add(new File('test_main.py', 600));
// $root->add($src);
// $root->add($tests);
// $root->add(new File('README.md', 300));

// $root->printTree();
// echo "Общий размер: " . $root->getSize() . " байт\n";

Задача 2. Корзина интернет-магазина

Реализуйте паттерн Компоновщик для корзины интернет-магазина. Листьевой элемент — Product (название, цена, количество). Контейнер — Bundle (набор товаров или других наборов, может иметь скидку в %). Оба реализуют интерфейс CartItem с методами total_price() и describe(indent). Скидка бандла применяется к итоговой сумме всех его содержимого.

from abc import ABC, abstractmethod

class CartItem(ABC):
    @abstractmethod
    def total_price(self) -> float: pass

    @abstractmethod
    def describe(self, indent: int = 0) -> str: pass

# TODO: реализовать Product(CartItem):
# - __init__(name: str, price: float, qty: int = 1)
# - total_price() -> price * qty
# - describe(indent) -> "  " * indent + f"{name} x{qty} = {total_price():.2f} руб."

# TODO: реализовать Bundle(CartItem):
# - __init__(name: str, discount_pct: float = 0.0)
# - add(item: CartItem), remove(item: CartItem)
# - total_price() -> сумма детей * (1 - discount_pct / 100)
# - describe(indent) -> выводит название бандла, скидку и рекурсивно детей

# Ожидаемый клиентский код:
# office_kit = Bundle("Офисный набор", discount_pct=10)
# office_kit.add(Product("Ручка", 50, qty=3))
# office_kit.add(Product("Тетрадь", 80, qty=2))

# gaming = Bundle("Игровой комплект", discount_pct=5)
# gaming.add(Product("Мышь", 2500))
# gaming.add(Product("Коврик", 800))
# gaming.add(office_kit)   # вложенный бандл

# cart = Bundle("Корзина")
# cart.add(gaming)
# cart.add(Product("HDMI кабель", 350))

# print(cart.describe())
# print(f"Итого: {cart.total_price():.2f} руб.")
<?php
interface CartItem {
    public function totalPrice(): float;
    public function describe(int $indent = 0): string;
}

// TODO: реализовать Product implements CartItem:
// - __construct(string $name, float $price, int $qty = 1)
// - totalPrice() -> $price * $qty
// - describe($indent) -> str_repeat('  ', $indent) . "{name} x{qty} = {total} руб."

// TODO: реализовать Bundle implements CartItem:
// - __construct(string $name, float $discountPct = 0.0)
// - add(CartItem $item), remove(CartItem $item)
// - totalPrice() -> сумма детей * (1 - discountPct / 100)
// - describe($indent) -> выводит название, скидку и рекурсивно детей

// Ожидаемый клиентский код:
// $officeKit = new Bundle('Офисный набор', discountPct: 10);
// $officeKit->add(new Product('Ручка', 50, qty: 3));
// $officeKit->add(new Product('Тетрадь', 80, qty: 2));

// $gaming = new Bundle('Игровой комплект', discountPct: 5);
// $gaming->add(new Product('Мышь', 2500));
// $gaming->add(new Product('Коврик', 800));
// $gaming->add($officeKit);

// $cart = new Bundle('Корзина');
// $cart->add($gaming);
// $cart->add(new Product('HDMI кабель', 350));

// echo $cart->describe();
// echo "Итого: " . number_format($cart->totalPrice(), 2) . " руб.\n";

Задача 3. Организационная иерархия

Реализуйте паттерн Компоновщик для организационной структуры компании. Листьевой элемент — Employee (имя, должность, зарплата). Контейнер — Department (название, содержит сотрудников и другие отделы). Оба реализуют интерфейс OrgUnit с методами get_salary_budget(), get_headcount() и print_structure(indent).

from abc import ABC, abstractmethod

class OrgUnit(ABC):
    @abstractmethod
    def get_salary_budget(self) -> float: pass

    @abstractmethod
    def get_headcount(self) -> int: pass

    @abstractmethod
    def print_structure(self, indent: int = 0) -> None: pass

# TODO: реализовать Employee(OrgUnit):
# - __init__(name: str, position: str, salary: float)
# - get_salary_budget() -> salary
# - get_headcount() -> 1
# - print_structure(indent) -> "  " * indent + f"{name} [{position}] — {salary:.0f} руб."

# TODO: реализовать Department(OrgUnit):
# - __init__(name: str)
# - add(unit: OrgUnit), remove(unit: OrgUnit)
# - get_salary_budget() -> сумма бюджетов всех дочерних узлов
# - get_headcount() -> сумма числа сотрудников всех дочерних узлов
# - print_structure(indent) -> "  " * indent + f"[{name}]" и рекурсия по детям

# Ожидаемый клиентский код:
# dev = Department("Разработка")
# dev.add(Employee("Анна", "Tech Lead", 250000))
# dev.add(Employee("Иван", "Backend Dev", 180000))
# dev.add(Employee("Мария", "Frontend Dev", 170000))

# qa = Department("QA")
# qa.add(Employee("Пётр", "QA Lead", 200000))
# qa.add(Employee("Света", "QA Engineer", 150000))

# it = Department("IT-дирекция")
# it.add(dev)
# it.add(qa)
# it.add(Employee("Алексей", "CTO", 400000))

# it.print_structure()
# print(f"Бюджет IT: {it.get_salary_budget():.0f} руб.")
# print(f"Сотрудников: {it.get_headcount()}")
<?php
interface OrgUnit {
    public function getSalaryBudget(): float;
    public function getHeadcount(): int;
    public function printStructure(int $indent = 0): void;
}

// TODO: реализовать Employee implements OrgUnit:
// - __construct(string $name, string $position, float $salary)
// - getSalaryBudget() -> $salary
// - getHeadcount() -> 1
// - printStructure($indent) -> выводит имя, должность и зарплату

// TODO: реализовать Department implements OrgUnit:
// - __construct(string $name)
// - add(OrgUnit $unit), remove(OrgUnit $unit)
// - getSalaryBudget() -> сумма бюджетов всех дочерних узлов
// - getHeadcount() -> сумма числа сотрудников всех дочерних узлов
// - printStructure($indent) -> "[{name}]" и рекурсия по детям

// Ожидаемый клиентский код:
// $dev = new Department('Разработка');
// $dev->add(new Employee('Анна', 'Tech Lead', 250000));
// $dev->add(new Employee('Иван', 'Backend Dev', 180000));

// $qa = new Department('QA');
// $qa->add(new Employee('Пётр', 'QA Lead', 200000));
// $qa->add(new Employee('Света', 'QA Engineer', 150000));

// $it = new Department('IT-дирекция');
// $it->add($dev);
// $it->add($qa);
// $it->add(new Employee('Алексей', 'CTO', 400000));

// $it->printStructure();
// echo "Бюджет IT: " . number_format($it->getSalaryBudget(), 0, '.', ' ') . " руб.\n";
// echo "Сотрудников: " . $it->getHeadcount() . "\n";

Задача 4. Меню ресторана

Реализуйте паттерн Компоновщик для системы меню ресторана. Листьевой элемент — MenuItem (название, описание, цена, признак вегетарианского). Контейнер — MenuSection (категория, содержит блюда и подкатегории). Оба реализуют интерфейс MenuComponent. Реализуйте методы print_menu(), get_total_items() и filter_vegetarian(), возвращающий новое дерево только с вегетарианскими позициями.

from abc import ABC, abstractmethod

class MenuComponent(ABC):
    @abstractmethod
    def print_menu(self, indent: int = 0) -> None: pass

    @abstractmethod
    def get_total_items(self) -> int: pass

    @abstractmethod
    def filter_vegetarian(self) -> 'MenuComponent | None': pass

# TODO: реализовать MenuItem(MenuComponent):
# - __init__(name: str, description: str, price: float, vegetarian: bool = False)
# - print_menu(indent) -> выводит название, цену и "(вег.)" если vegetarian
# - get_total_items() -> 1
# - filter_vegetarian() -> self если vegetarian, иначе None

# TODO: реализовать MenuSection(MenuComponent):
# - __init__(name: str)
# - add(component: MenuComponent), remove(component: MenuComponent)
# - print_menu(indent) -> выводит название секции и рекурсивно детей
# - get_total_items() -> сумма get_total_items() всех детей
# - filter_vegetarian() -> новую MenuSection только с вегетарианскими детьми
#   (если у секции нет вегетарианских детей — возвращает None)

# Ожидаемый клиентский код:
# menu = MenuSection("Главное меню")
# breakfast = MenuSection("Завтраки")
# breakfast.add(MenuItem("Овсянка", "С ягодами", 290, vegetarian=True))
# breakfast.add(MenuItem("Яичница", "С беконом", 350))
# breakfast.add(MenuItem("Авокадо тост", "На ржаном хлебе", 420, vegetarian=True))

# mains = MenuSection("Основные блюда")
# mains.add(MenuItem("Стейк", "Говядина рибай", 1800))
# mains.add(MenuItem("Паста примавера", "Овощи и соус песто", 650, vegetarian=True))

# menu.add(breakfast)
# menu.add(mains)

# menu.print_menu()
# print(f"\nВсего позиций: {menu.get_total_items()}")
# veg_menu = menu.filter_vegetarian()
# print("\n--- Вегетарианское меню ---")
# veg_menu.print_menu()
<?php
interface MenuComponent {
    public function printMenu(int $indent = 0): void;
    public function getTotalItems(): int;
    public function filterVegetarian(): ?MenuComponent;
}

// TODO: реализовать MenuItem implements MenuComponent:
// - __construct(string $name, string $description, float $price, bool $vegetarian = false)
// - printMenu($indent) -> выводит название, цену и "(вег.)" если $vegetarian
// - getTotalItems() -> 1
// - filterVegetarian() -> $this если $vegetarian, иначе null

// TODO: реализовать MenuSection implements MenuComponent:
// - __construct(string $name)
// - add(MenuComponent $component), remove(MenuComponent $component)
// - printMenu($indent) -> выводит название секции и рекурсивно детей
// - getTotalItems() -> сумма getTotalItems() всех детей
// - filterVegetarian() -> новую MenuSection только с вегетарианскими детьми
//   (если нет вегетарианских детей — возвращает null)

// Ожидаемый клиентский код:
// $menu = new MenuSection('Главное меню');
// $breakfast = new MenuSection('Завтраки');
// $breakfast->add(new MenuItem('Овсянка', 'С ягодами', 290, vegetarian: true));
// $breakfast->add(new MenuItem('Яичница', 'С беконом', 350));
// $breakfast->add(new MenuItem('Авокадо тост', 'На ржаном хлебе', 420, vegetarian: true));

// $mains = new MenuSection('Основные блюда');
// $mains->add(new MenuItem('Стейк', 'Говядина рибай', 1800));
// $mains->add(new MenuItem('Паста примавера', 'Овощи и соус песто', 650, vegetarian: true));

// $menu->add($breakfast);
// $menu->add($mains);

// $menu->printMenu();
// echo "\nВсего позиций: " . $menu->getTotalItems() . "\n";
// $vegMenu = $menu->filterVegetarian();
// echo "\n--- Вегетарианское меню ---\n";
// $vegMenu->printMenu();

Задача 5. Дерево задач проекта

Реализуйте паттерн Компоновщик для системы управления задачами. Листьевой элемент — Task (название, исполнитель, оценка в часах, статус). Контейнер — Epic (эпик или спринт, содержит задачи и другие эпики). Оба реализуют интерфейс WorkItem. Реализуйте методы get_progress() (доля выполненных задач в %), get_total_hours() и find_by_assignee(name).

from abc import ABC, abstractmethod
from enum import Enum

class Status(Enum):
    TODO = "todo"
    IN_PROGRESS = "in_progress"
    DONE = "done"

class WorkItem(ABC):
    @abstractmethod
    def get_total_hours(self) -> float: pass

    @abstractmethod
    def get_done_count(self) -> int: pass

    @abstractmethod
    def get_total_count(self) -> int: pass

    @abstractmethod
    def find_by_assignee(self, name: str) -> list['WorkItem']: pass

    @abstractmethod
    def print_tree(self, indent: int = 0) -> None: pass

    def get_progress(self) -> float:
        total = self.get_total_count()
        return (self.get_done_count() / total * 100) if total else 0.0

# TODO: реализовать Task(WorkItem):
# - __init__(title: str, assignee: str, hours: float, status: Status = Status.TODO)
# - get_total_hours() -> hours
# - get_done_count() -> 1 если status == DONE, иначе 0
# - get_total_count() -> 1
# - find_by_assignee(name) -> [self] если self.assignee == name, иначе []
# - print_tree(indent) -> "  " * indent + f"[{status.value}] {title} ({assignee}, {hours}h)"

# TODO: реализовать Epic(WorkItem):
# - __init__(name: str)
# - add(item: WorkItem), remove(item: WorkItem)
# - методы делегируют рекурсию к дочерним элементам

# Ожидаемый клиентский код:
# sprint1 = Epic("Sprint 1")
# auth = Epic("Авторизация")
# auth.add(Task("Форма входа",      "Анна",  8, Status.DONE))
# auth.add(Task("JWT токены",       "Иван",  6, Status.DONE))
# auth.add(Task("OAuth интеграция", "Анна", 12, Status.IN_PROGRESS))

# api = Epic("REST API")
# api.add(Task("CRUD пользователей", "Иван",  10, Status.DONE))
# api.add(Task("Фильтрация",         "Мария",  6, Status.TODO))

# sprint1.add(auth)
# sprint1.add(api)

# sprint1.print_tree()
# print(f"\nПрогресс: {sprint1.get_progress():.1f}%")
# print(f"Часов всего: {sprint1.get_total_hours()}")
# print(f"Задачи Анны: {[t.title for t in sprint1.find_by_assignee('Анна')]}")

```php linenums="1" <?php enum Status: string { case TODO = 'todo'; case IN_PROGRESS = 'in_progress'; case DONE = 'done'; }

interface WorkItem { public function getTotalHours(): float; public function getDoneCount(): int; public function getTotalCount(): int; public function findByAssignee(string $name): array; public function printTree(int $indent = 0): void;

public function getProgress(): float;

}

// TODO: реализовать Task implements WorkItem: // - __construct(string $title, string $assignee, float $hours, Status $status = Status::TODO) // - getTotalHours() -> $hours // - getDoneCount() -> 1 если \(status === Status::DONE, иначе 0 // - getTotalCount() -> 1 // - findByAssignee(\)name) -> [$this] если $this->assignee === \(name, иначе [] // - printTree(\)indent) -> выводит статус, название, исполнителя и часы // - getProgress() -> 100.0 если DONE, иначе 0.0

// TODO: реализовать Epic implements WorkItem: // - __construct(string $name) // - add(WorkItem $item), remove(WorkItem $item) // - методы делегируют рекурсию к дочерним элементам // - getProgress() -> getDoneCount() / getTotalCount() * 100

// Ожидаемый клиентский код: // $sprint1 = new Epic('Sprint 1'); // $auth = new Epic('Авторизация'); // $auth->add(new Task('Форма входа', 'Анна', 8, Status::DONE)); // $auth->add(new Task('JWT токены', 'Иван', 6, Status::DONE)); // $auth->add(new Task('OAuth интеграция', 'Анна', 12, Status::IN_PROGRESS));

// $api = new Epic('REST API'); // $api->add(new Task('CRUD пользователей', 'Иван', 10, Status::DONE)); // $api->add(new Task('Фильтрация', 'Мария', 6, Status::TODO));

// \(sprint1->add(\)auth); // \(sprint1->add(\)api);

// \(sprint1->printTree(); // echo "\nПрогресс: " . number_format(\)sprint1->getProgress(), 1) . "%\n"; // echo "Часов всего: " . $sprint1->getTotalHours() . "\n";