Menu
Coddy logo textTech

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

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

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

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

#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;
};

// Leaf - представляет отдельные файлы
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; }
};

// Composite - содержит другие компоненты
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 хранит children и реализует операции, перебирая их. Когда вы вызываете getSize() для папки, он рекурсивно вычисляет общий размер всех содержащихся элементов. Клиентскому коду не нужно знать, работает ли он с файлом или папкой — оба отвечают на один и тот же интерфейс.

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

challenge icon

Задание

Легко

Давайте создадим систему Organization Chart с использованием паттерна Composite. Вы создадите иерархию, в которой как отдельные employees, так и departments (содержащие других employees или sub-departments) могут обрабатываться единообразно. Это отражает структуру реальных компаний: departments содержат людей и другие departments, образуя древовидную структуру.

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

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

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

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

    Добавьте виртуальный деструктор.

  • Organization.h: реализуйте leaf- и composite-классы.

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

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

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

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

    Формат отображения department должен быть таким:

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

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

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

    Создайте следующую структуру: создайте company (department верхнего уровня), добавьте в неё sub-department и добавьте обоих employees в этот sub-department. Затем отобразите всю organization и выведите общую зарплату company.

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

    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(): он единообразно обрабатывает вызов как для employee, так и для department — department автоматически суммирует зарплаты всех своих участников. Клиентскому коду не нужно различать отдельных employees и целые departments при вычислении итогов или отображении иерархии.

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

#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: Создать суб-департамент
    
    // TODO: Создать два объекта Employee, используя разобранные данные
    
    // TODO: Добавить сотрудников в суб-департамент
    
    // TODO: Добавить суб-департамент в компанию
    
    // TODO: Отобразить всю структуру организации
    
    // TODO: Вывести общую зарплату в формате: Total Salary: $[amount]
    
    return 0;
}
quiz iconПроверьте себя

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

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

Потренируйтесь самостоятельно: Онлайн-компилятор C++