Menu
Coddy logo textTech

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

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

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

Паттерн состоит из трех основных частей: интерфейса Component, который определяет общие операции, классов Leaf, представляющих отдельные объекты, и классов Composite, которые содержат дочерние элементы и делегируют им операции:

#include <iostream>
#include <memory>
#include <vector>
#include <string>

// Интерфейс компонента
class FileSystemItem {
public:
    virtual void display(int indent = 0) const = 0;
    virtual int getSize() const = 0;
    virtual ~FileSystemItem() = default;
};

// Лист — представляет отдельные файлы
class File : public FileSystemItem {
    std::string name;
    int size;
public:
    File(const std::string& n, int s) : name(n), size(s) {}
    
    void display(int indent = 0) const override {
        std::cout << std::string(indent, ' ') << name 
                  << " (" << size << " KB)\n";
    }
    int getSize() const override { return size; }
};

// Композит — содержит другие компоненты
class Folder : public FileSystemItem {
    std::string name;
    std::vector<std::shared_ptr<FileSystemItem>> children;
public:
    Folder(const std::string& n) : name(n) {}
    
    void add(std::shared_ptr<FileSystemItem> item) {
        children.push_back(item);
    }
    
    void display(int indent = 0) const override {
        std::cout << std::string(indent, ' ') << "[" << name << "]\n";
        for (const auto& child : children) {
            child->display(indent + 2);
        }
    }
    
    int getSize() const override {
        int total = 0;
        for (const auto& child : children) {
            total += child->getSize();
        }
        return total;
    }
};

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

Используйте Компоновщик, когда вам нужно представить иерархии «целое-часть» и вы хотите, чтобы клиенты одинаково трактовали как отдельные объекты, так и их композиции.

challenge icon

Задание

Легко

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

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

  • OrgComponent.h: Определите интерфейс компонента, который будут реализовывать как сотрудники, так и отделы.

    Создайте абстрактный класс OrgComponent с методами:

    • getName() — возвращает имя компонента
    • getSalary() — возвращает общую зарплату (для сотрудников — их собственную зарплату; для отделов — сумму всех зарплат внутри него)
    • display(int indent = 0) — отображает компонент с соответствующим отступом

    Включите виртуальный деструктор.

  • Organization.h: Реализуйте классы "листа" и "компоновщика".

    Создайте класс Employee (лист), который хранит имя и зарплату. Его метод display() должен выводить информацию о сотруднике в следующем формате:

    [indent spaces]- [name] ($[salary])

    Создайте класс Department (компоновщик), который хранит имя и коллекцию дочерних элементов OrgComponent, используя std::shared_ptr. Реализуйте:

    • add(std::shared_ptr<OrgComponent> component) — добавляет дочерний элемент в отдел
    • getSalary() — рекурсивно вычисляет общую зарплату всех участников
    • display() — выводит название отдела в скобках, затем отображает всех дочерних элементов с увеличенным отступом (добавляйте по 2 пробела на каждый уровень)

    Формат отображения отдела должен быть следующим:

    [indent spaces][Department Name]
      [children displayed with indent + 2]
  • main.cpp: Постройте и отобразите структуру организации.

    Прочитайте четыре входных значения:

    1. Название компании (строка)
    2. Название отдела (строка)
    3. Имя и зарплата первого сотрудника (формат: name,salary)
    4. Имя и зарплата второго сотрудника (формат: name,salary)

    Постройте следующую структуру: создайте компанию (отдел верхнего уровня), добавьте в нее подотдел и добавьте обоих сотрудников в этот подотдел. Затем отобразите всю организацию и выведите общую зарплату компании.

    После отображения структуры выведите:

    Total Salary: $[amount]

Например, при входных данных TechCorp, Engineering, Alice,75000 и Bob,65000:

[TechCorp]
  [Engineering]
    - Alice ($75000)
    - Bob ($65000)
Total Salary: $140000

При входных данных StartupInc, Development, Carol,80000 и Dave,70000:

[StartupInc]
  [Development]
    - Carol ($80000)
    - Dave ($70000)
Total Salary: $150000

Обратите внимание, как getSalary() работает одинаково, независимо от того, вызывается ли он для сотрудника или для отдела — отдел автоматически суммирует зарплаты всех своих участников. Клиентскому коду не нужно различать отдельных сотрудников и целые отделы при расчете итогов или отображении иерархии.

Шпаргалка

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

Паттерн состоит из трех ключевых частей:

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

Пример реализации файловой системы:

#include <iostream>
#include <memory>
#include <vector>
#include <string>

// Интерфейс компонента
class FileSystemItem {
public:
    virtual void display(int indent = 0) const = 0;
    virtual int getSize() const = 0;
    virtual ~FileSystemItem() = default;
};

// Лист — представляет отдельные файлы
class File : public FileSystemItem {
    std::string name;
    int size;
public:
    File(const std::string& n, int s) : name(n), size(s) {}
    
    void display(int indent = 0) const override {
        std::cout << std::string(indent, ' ') << name 
                  << " (" << size << " KB)\n";
    }
    int getSize() const override { return size; }
};

// Компоновщик — содержит другие компоненты
class Folder : public FileSystemItem {
    std::string name;
    std::vector<std::shared_ptr<FileSystemItem>> children;
public:
    Folder(const std::string& n) : name(n) {}
    
    void add(std::shared_ptr<FileSystemItem> item) {
        children.push_back(item);
    }
    
    void display(int indent = 0) const override {
        std::cout << std::string(indent, ' ') << "[" << name << "]\n";
        for (const auto& child : children) {
            child->display(indent + 2);
        }
    }
    
    int getSize() const override {
        int total = 0;
        for (const auto& child : children) {
            total += child->getSize();
        }
        return total;
    }
};

Компоновщик хранит дочерние элементы, используя std::shared_ptr, и реализует операции путем итерации по ним. Такие операции, как getSize(), работают рекурсивно, вычисляя общие значения по всей иерархии.

Используйте Компоновщик, когда вам нужно представить иерархию «часть-целое» и вы хотите, чтобы клиенты одинаково трактовали как отдельные объекты, так и их совокупности.

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

#include <iostream>
#include <string>
#include <memory>
#include <sstream>
#include "Organization.h"

int main() {
    // Чтение входных данных
    std::string companyName;
    std::string departmentName;
    std::string employee1Input;
    std::string employee2Input;
    
    std::getline(std::cin, companyName);
    std::getline(std::cin, departmentName);
    std::getline(std::cin, employee1Input);
    std::getline(std::cin, employee2Input);
    
    // Вспомогательное лямбда-выражение для парсинга формата "name,salary"
    auto parseEmployee = [](const std::string& input) -> std::pair<std::string, int> {
        size_t commaPos = input.find(',');
        std::string name = input.substr(0, commaPos);
        int salary = std::stoi(input.substr(commaPos + 1));
        return {name, salary};
    };
    
    auto [name1, salary1] = parseEmployee(employee1Input);
    auto [name2, salary2] = parseEmployee(employee2Input);
    
    // TODO: Создайте компанию как Department верхнего уровня
    
    // TODO: Создайте подразделение (sub-department)
    
    // TODO: Создайте два объекта Employee, используя полученные данные
    
    // TODO: Добавьте сотрудников в подразделение
    
    // TODO: Добавьте подразделение в компанию
    
    // TODO: Выведите всю структуру организации
    
    // TODO: Выведите общую зарплату в формате: Total Salary: $[amount]
    
    return 0;
}
quiz iconПроверьте себя

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

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