Паттерн Компоновщик
Часть раздела Объектно-ориентированное программирование путешествия по 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() для папки, он рекурсивно вычисляет общий размер всех содержащихся в ней элементов. Клиентскому коду не нужно знать, работает ли он с файлом или папкой — оба объекта поддерживают один и тот же интерфейс.
Используйте Компоновщик, когда вам нужно представить иерархии «целое-часть» и вы хотите, чтобы клиенты одинаково трактовали как отдельные объекты, так и их композиции.
Задание
ЛегкоДавайте создадим систему Организационной структуры, используя паттерн Компоновщик (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: Постройте и отобразите структуру организации.Прочитайте четыре входных значения:
- Название компании (строка)
- Название отдела (строка)
- Имя и зарплата первого сотрудника (формат:
name,salary) - Имя и зарплата второго сотрудника (формат:
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;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыСборка и компиляция C++Заголовочные файлы и файлы исходного кодаПространства имен и область видимостиВведение в ООП на C++Классы и объектыУказатель 'this'Методы (функции-члены)Атрибуты (члены данных)Основы конструкторов и деструкторовИтоги — Простой калькулятор4Свойства классов
Члены экземпляра против статическихГеттеры и сеттерыКонстантные функции-членыКлючевое слово mutableСтатические методы и переменныеДружественные функции и классыИтоги — Менеджер банковских счетов7Наследование
Основы наследованияУровни доступа при наследованииПорядок вызова конструкторов и деструкторовПереопределение методовВиртуальные функции и VTableМножественное наследованиеВиртуальное наследованиеИтоги — Иерархия сотрудников10Обзор STL
Обзор и философия STLКонтейнеры STLИтераторыАлгоритмы STLФункторы и лямбда-выраженияИтоги: частота слов13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonFactory и Abstract FactoryПаттерн BuilderПаттерн ObserverПаттерн Strategy2Управление памятью
Стек и кучаУказатели и ссылкиДинамическая память (new/delete)Умные указатели в C++RAII в C++Итоги — Менеджер динамического массива5Инкапсуляция
Спецификаторы доступа в C++Спецификаторы доступа: подробный разборСокрытие данныхStruct против ClassВложенные и внутренние классыИтоги — Система учета студентов8Полиморфизм
Полиморфизм компиляции и времени выполненияПерегрузка функцийВиртуальные функции: повторениеЧистые виртуальные функцииАбстрактные классыПроектирование интерфейсов на C++Dynamic Casting и RTTIИтоги: Калькулятор фигур11Продвинутые концепции ООП
Композиция против наследованияМиксины через CRTPИдиома PimplСтирание типовEnum Classes и строгая типизацияОбработка исключений в ООППользовательские иерархии исключений14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикRAII как паттерн3Конструкторы и деструкторы
Конструктор по умолчаниюПараметризованный конструкторКонструктор копированияКонструктор перемещенияСписки инициализации конструктораДелегирующие конструкторыГлубокое погружение в деструкторыПравило трех / пяти / нуляПовторение — класс String6Перегрузка операторов
Введение в перегрузку операторовПерегрузка арифметических операторовПерегрузка операторов сравненияОператоры потоковПерегрузка оператора присваиванияПерегрузка операторов [] и ()Операторы преобразования типовИтоги — класс Matrix9Шаблоны
Шаблоны функцийШаблоны классовСпециализация шаблоновВариативные шаблоныОсновы SFINAE и Type TraitsИтоги — Обобщенный контейнер