Введение
Многие реальные предметные области естественно моделируются как иерархия «часть-целое» — файловая система (директории, содержащие файлы и другие директории), графический интерфейс (контейнеры виджетов, содержащие другие виджеты, статьи QML-цикла про вложенность компонентов), организационная структура — Composite даёт единообразный способ работы и с отдельными, «листовыми» элементами такой иерархии, и с составными, содержащими другие элементы, через единый, общий интерфейс.
Концепция
Composite определяет общий интерфейс (часто называемый Component) для и листовых элементов (Leaf, не содержащих дочерних элементов), и составных элементов (Composite, содержащих коллекцию дочерних элементов того же общего интерфейса Component) — клиентский код, работающий через этот общий интерфейс, может единообразно обрабатывать как отдельный, листовой элемент, так и целое поддерево составных элементов, не различая их явно (составной элемент обычно просто рекурсивно делегирует операцию каждому из своих дочерних элементов, которые могут сами быть либо листьями, либо снова составными элементами).
Пример кода
#include <iostream>
#include <memory>
#include <vector>
#include <string>
class FileSystemItem // общий интерфейс — Component
{
public:
virtual void display(int indent = 0) const = 0;
virtual long size() const = 0;
virtual ~FileSystemItem() = default;
};
class File : public FileSystemItem // Leaf — листовой элемент, без дочерних элементов
{
public:
File(const std::string &name, long fileSize) : m_name(name), m_size(fileSize) {}
void display(int indent) const override
{
std::cout << std::string(indent, ' ') << "📄 " << m_name << " (" << m_size << " байт)" << std::endl;
}
long size() const override { return m_size; }
private:
std::string m_name;
long m_size;
};
class Directory : public FileSystemItem // Composite — содержит ДРУГИЕ FileSystemItem (Files или Directories)
{
public:
explicit Directory(const std::string &name) : m_name(name) {}
void add(std::unique_ptr<FileSystemItem> item) { m_items.push_back(std::move(item)); }
void display(int indent) const override
{
std::cout << std::string(indent, ' ') << "📁 " << m_name << "/" << std::endl;
for (const auto &item : m_items) {
item->display(indent + 2); // РЕКУРСИВНОЕ делегирование — каждый элемент сам знает, как себя отобразить
}
}
long size() const override
{
long total = 0;
for (const auto &item : m_items) {
total += item->size(); // РЕКУРСИВНОЕ суммирование — работает ОДИНАКОВО для File и Directory
}
return total;
}
private:
std::string m_name;
std::vector<std::unique_ptr<FileSystemItem>> m_items;
};
int main()
{
auto root = std::make_unique<Directory>("project");
root->add(std::make_unique<File>("main.cpp", 1500));
auto srcDir = std::make_unique<Directory>("src");
srcDir->add(std::make_unique<File>("utils.cpp", 800));
srcDir->add(std::make_unique<File>("utils.h", 300));
root->add(std::move(srcDir)); // Directory ВНУТРИ Directory — естественная рекурсивная структура
root->display();
std::cout << "Общий размер: " << root->size() << " байт" << std::endl;
return 0;
}
Пояснения к коду
Directory::size(), вызывающий item->size() для каждого дочернего элемента независимо от того, является ли этот элемент File (листом) или другим Directory (составным элементом), показывает суть паттерна — благодаря единому интерфейсу FileSystemItem код Directory не нуждается в явной проверке типа каждого дочернего элемента, полиморфный вызов автоматически выбирает правильную реализацию , и рекурсия естественным образом распространяется через произвольную глубину вложенности директорий. Структура с srcDir, добавленным внутрь root (директория внутри директории), показывает естественное построение древовидной иерархии произвольной глубины — каждый узел этого дерева, будь то файл или директория, обрабатывается через тот же общий интерфейс FileSystemItem, что и позволяет вызову root->display()/root->size() корректно обработать ВСЮ иерархию произвольной глубины одним простым, не зависящим от конкретной глубины вложенности вызовом.
Подводные камни
- Добавление в общий интерфейс Component методов, осмысленных только для Composite (составных элементов), но не для Leaf (листовых элементов) — например, метод
add()(добавление дочернего элемента) логически не применим кFile, и если разместить его в общем интерфейсеFileSystemItem,Fileдолжен был бы либо реализовать его бессмысленным, «пустым» образом, либо выбросить исключение при попытке вызова, что нарушает принцип подстановки (аналогично подводным камням статьи цикла синтаксиса про корректность иерархий наследования) — методы, специфичные только для составных элементов, обычно лучше оставлять только в классе Composite, а не в общем интерфейсе. - Игнорирование вычислительной стоимости рекурсивных операций для очень глубоких или широких древовидных структур — операции, подобные
size()в примере, требуют обхода всего поддерева, и для очень больших, глубоких иерархий (тысячи файлов в сложной структуре директорий) это может стать заметной по времени операцией, особенно если она выполняется часто, и для таких случаев может быть оправданно кеширование результата (с явным инвалидированием кеша при изменении структуры) вместо пересчёта «с нуля» при каждом обращении. - Циклическая структура внутри Composite (составной элемент, прямо или транзитивно содержащий ссылку на самого себя как одного из своих дочерних элементов) — это привело бы к бесконечной рекурсии при попытке любой операции обхода всего поддерева (аналогично подводным камням статьи алгоритмического цикла про необходимость отслеживания посещённых вершин при обходе графа с возможными циклами), и для структур, теоретически допускающих такие циклы (в отличие от строго древовидной, заведомо ацикличной файловой системы), нужна явная защита от подобной бесконечной рекурсии.
- Использование «сырых» указателей для хранения коллекции дочерних элементов вместо умных указателей (как
std::unique_ptrв примере) — поскольку составной элемент Composite обычно владеет своими дочерними элементами (отвечает за их время жизни), отсутствие явного, корректного управления этим владением через умные указатели рискует утечками памяти при удалении составного элемента без должного освобождения всех вложенных в него дочерних элементов.