Menu
Coddy logo textTech

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

Часть раздела Объектно-ориентированное программирование путешествия по PHP на Coddy — урок 80 из 91.

Паттерн Компоновщик (Composite Pattern) — это структурный паттерн проектирования, который позволяет группировать объекты в древовидные структуры и работать с этими структурами так, как если бы они были отдельными объектами. Он идеально подходит для представления иерархий «часть-целое», где вы хотите единообразно обрабатывать как одиночные объекты, так и группы объектов.

Рассмотрим файловую систему. Папка может содержать файлы и другие папки.

Когда вы вычисляете общий размер, вы хотите вызывать один и тот же метод как для файлов, так и для папок, при этом папки должны рекурсивно суммировать свое содержимое. Паттерн Компоновщик (Composite Pattern) делает этот процесс бесшовным, предоставляя как листьям (файлам), так и составным объектам (папкам) один и тот же интерфейс.

<?php
interface FileSystemItem {
    public function getName(): string;
    public function getSize(): int;
}

class File implements FileSystemItem {
    public function __construct(
        private string $name,
        private int $size
    ) {}
    
    public function getName(): string {
        return $this->name;
    }
    
    public function getSize(): int {
        return $this->size;
    }
}

class Folder implements FileSystemItem {
    private array $items = [];
    
    public function __construct(private string $name) {}
    
    public function getName(): string {
        return $this->name;
    }
    
    public function add(FileSystemItem $item): void {
        $this->items[] = $item;
    }
    
    public function getSize(): int {
        $total = 0;
        foreach ($this->items as $item) {
            $total += $item->getSize();
        }
        return $total;
    }
}

Теперь вы можете создавать древовидные структуры и работать с ними единообразно:

<?php
$docs = new Folder("Documents");
$docs->add(new File("resume.pdf", 250));
$docs->add(new File("cover.docx", 50));

$images = new Folder("Images");
$images->add(new File("photo.jpg", 1500));

$root = new Folder("Root");
$root->add($docs);
$root->add($images);
$root->add(new File("readme.txt", 10));

echo $root->getSize();

Вывод:

1810

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

challenge icon

Задание

Легко

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

Вы распределите свой код по четырем файлам:

  • OrganizationUnit.php — Определите интерфейс OrganizationUnit, который будут реализовывать как отдельные сотрудники, так и отделы. Он должен иметь два метода: getName(): string и getSalary(): int. Этот общий интерфейс позволяет вам одинаково относиться к отдельным сотрудникам и целым отделам при расчете затрат.
  • Employee.php — Подключите интерфейс OrganizationUnit и создайте класс Employee, который его реализует. Сотрудник является листовым узлом в дереве вашей организации — у него есть имя и зарплата, но он не может содержать другие элементы. Используйте продвижение свойств в конструкторе (constructor promotion) для принятия имени и зарплаты и реализуйте оба метода интерфейса.
  • Department.php — Подключите интерфейс OrganizationUnit и создайте класс Department, который его реализует. Отдел — это компоновщик, который может содержать как сотрудников, так и другие отделы. Ваш отдел должен:
    • Принимать имя в конструкторе
    • Иметь метод add(OrganizationUnit $unit): void для добавления сотрудников или подотделов
    • Реализовать getSalary() для возврата общей зарплаты всех содержащихся в нем единиц (рекурсивно суммируя все внутри)
  • main.php — Подключите файлы Employee и Department. Вы получите один входной параметр: строку JSON, представляющую структуру организации.

    JSON будет в следующем формате:

    {"Engineering": [{"name": "Alice", "salary": 80000}, {"name": "Bob", "salary": 75000}], "Sales": [{"name": "Carol", "salary": 65000}]}

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

    Создайте корневой Department с названием "Company". Для каждого отдела в JSON создайте Department, добавьте в него сотрудников, а затем добавьте этот отдел в Company. Наконец, выведите общую зарплату компании в следующем формате:

    Total Salary: [amount]

Прелесть паттерна Компоновщик заключается в том, что вызов getSalary() для отдела Company автоматически вычисляет сумму по всем вложенным отделам и сотрудникам — клиентскому коду не нужно знать внутреннюю структуру.

Шпаргалка

Паттерн Компоновщик (Composite Pattern) — это структурный паттерн проектирования, который объединяет объекты в древовидные структуры и позволяет работать с отдельными объектами и группами объектов единообразно.

Он идеально подходит для иерархий «часть-целое», где требуется одинаковый интерфейс как для листовых узлов, так и для составных узлов.

Основные компоненты

  • Интерфейс компонента (Component Interface) — определяет общие операции как для листьев, так и для компоновщиков
  • Лист (Leaf) — представляет собой отдельные объекты, которые не могут содержать другие объекты
  • Компоновщик (Composite) — содержит дочерние элементы (листья или другие компоновщики) и реализует операции, делегируя их выполнение дочерним элементам

Пример: Файловая система

<?php
interface FileSystemItem {
    public function getName(): string;
    public function getSize(): int;
}

class File implements FileSystemItem {
    public function __construct(
        private string $name,
        private int $size
    ) {}
    
    public function getName(): string {
        return $this->name;
    }
    
    public function getSize(): int {
        return $this->size;
    }
}

class Folder implements FileSystemItem {
    private array $items = [];
    
    public function __construct(private string $name) {}
    
    public function getName(): string {
        return $this->name;
    }
    
    public function add(FileSystemItem $item): void {
        $this->items[] = $item;
    }
    
    public function getSize(): int {
        $total = 0;
        foreach ($this->items as $item) {
            $total += $item->getSize();
        }
        return $total;
    }
}

Использование

<?php
$docs = new Folder("Documents");
$docs->add(new File("resume.pdf", 250));
$docs->add(new File("cover.docx", 50));

$images = new Folder("Images");
$images->add(new File("photo.jpg", 1500));

$root = new Folder("Root");
$root->add($docs);
$root->add($images);
$root->add(new File("readme.txt", 10));

echo $root->getSize(); // 1810

Клиент вызывает getSize() единообразно как для файлов, так и для папок. Компоновщик обрабатывает рекурсию внутри себя, что упрощает работу со сложными иерархиями.

Попробуйте сами

<?php
require_once 'Employee.php';
require_once 'Department.php';

// Чтение входных данных JSON
$input = trim(fgets(STDIN));
$data = (array)json_decode($input, true);

// TODO: Создать корневой отдел под названием "Company"

// TODO: Пройти циклом по каждому отделу в данных JSON
// Для каждого отдела:
//   - Создать новый объект Department
//   - Добавить каждого сотрудника в этот отдел
//   - Добавить отдел в Company

// TODO: Вывести общую зарплату в формате: Total Salary: [amount]

?>
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование