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

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

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

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

  1. Добавить вложенный элемент;
  2. Извлечь результат.

Сначала дерево компонуется из составных частей с помощью операции добавления. А затем для элемента, находящегося на вершине полученного дерева, извлекается результат. Причем, вершина дерева зависит от вложенных в него узлов. А эти узлы зависят от вложенных в них узлов. И так до самого низа, т.е. до листьев дерева.

Косвенно мы уже сталкивались с Компоновщиком, когда рассматривали паттерн Строитель. В тот раз мы занимались генерацией XML-кода. Сам Компоновщик был скрыт за интерфейсом стандартной библиотеки QtXml.

Пример использования паттерна Компоновщик

Создадим приложение, способное генерировать простой код на C++ следующего вида:

Следует понимать, что полноценный компилятор — это очень сложная программа. Чтобы не увязнуть в деталях, и сконцентрироваться на самом Компоновщике, мы существенно упростим задачу. Обойдемся без проверки корректности синтаксиса. А также оставим всего три элемента языка: класс, функция-член и операция вывода на консоль.

Нарисуем дерево синтаксического разбора, соответствующее фрагменту кода выше:

composite-pattern-example-diagram-thumbnail

Базовый класс элемента синтаксиса

Определим абстрактный класс Unit, представляющий некую языковую конструкцию:

Виртуальная функция-член add() предназначена для добавления вложенных элементов (передача происходит через умный указатель std::shared_ptr). Также эта функция принимает параметр Flags. Совсем скоро мы посмотрим на его применение. По умолчанию add() выбрасывает исключение.

Функция compile() генерирует код на C++, соответствующий содержимому элемента. Результат возвращается в виде строки std::string. В качестве аргумента функция принимает параметр level, указывающий на уровень вложенности узла дерева. Это требуется для корректной расстановки отступов в начале строк генерируемого кода.

Вспомогательная функция-член generateShift() всего лишь возвращает строку, состоящую из нужного числа пробелов. Результат зависит от уровня вложенности.

Реализация элемента Класс

Вот одна из возможных реализаций класса, представляющего Класс:

Имя Класса указывается при его создании через аргумент конструктора. При этом Класс хранит все свои поля в векторе std::vector. Обратите внимание, что используется не просто вектор, а вектор из трех векторов. По одному на каждый модификатор области видимости: public, protected и private.

Элемент Класса сам распределяет свои поля по областям видимости. Поэтому в качестве флага функция add() ожидает получить одно из значений нашего перечисления ClassUnit::AccessModifier. А затем помещает добавляемый элемент в нужный вектор.

Процесс «компиляции» Класса достаточно линеен, поэтому не требует особых объяснений. Единственное, обратите внимание на этот фрагмент:

В процессе компоновки происходит последовательный обход всех полей. Для них также вызывается функция compile() с уровнем level на единицу больше. А сформированный фрагмент кода присоединяется в конец результатирующей строки result.

Реализация элемента Метод

Представление Функции-члена реализуем в виде следующего класса:

Конструктор класса MethodUnit принимает имя функции, тип возвращаемого значения и флаги. Использование std::string в качестве типа — не самое удачное решение (сойдет только для примера). В полноценной системе Тип тоже должен быть представлен отдельным классом, наследующим Unit.

Для Функции-члена мы предусмотрели три возможных модификатора: STATIC, CONST и VIRTUAL. Они определены в перечислении Modifier в виде битовых флагов. Их комбинацию мы и ожидаем получать в качестве третьего аргумента конструктора flags.

При добавлении элементов с помощью add() не требуется указывать дополнительные флаги. Мы просто помещаем все в конец вектора m_body, заполняя тело Функции.

Генерация кода в compile() напоминает то, что мы уже видели для элемента Класса. Конечно, со своим специфическим синтаксисом. Например, обратите внимание на то, как используются битовые флаги модификатора m_flags.

Реализация элемента «Оператор печати»

А здесь все совсем просто:

Думаю, комментарии излишни. Идем дальше.

Компоновщик в действии

А теперь скомпонуем программу, которую собирались изначально:

Осталось только вызвать эту функцию:

После запуска приложения на консоль будет выведено ровно то, что мы и хотели.

Сейчас мы компонуем синтаксическое дерево вручную. В реальном приложении мы бы могли придумать свой высокоуровневый (по сравнению с C++) синтаксис для решения каких-то специфических задач. А затем использовать рассмотренный подход для трансляции нашего упрощенного кода в код C++. Мы затрагивали подобные идеи, когда обсуждали принцип DRY.

С учетом наших допущений получилось не так уж и плохо. Но остается серьезный недостаток. Сейчас функция generateProgram() жестко привязана к формированию кода на C++. Однако при определенных ограничениях мы могли бы обеспечить генерацию кода и на других языках программирования. Этим мы займемся в следующий раз. А поможет нам в этом паттерн Абстрактная фабрика.

Понравилась статья? Поделиться с друзьями:
Добавить комментарий