Паттерн Посетитель на C++

Паттерн Посетитель на C++

Рекомендую сначала ознакомиться с двумя предыдущими частями:

  1. Паттерн Компоновщик на C++
  2. Паттерн Абстрактная фабрика на C++

С концепцией Посетителя мы уже сталкивались, когда говорили об указателях на функции в C++. В этой статье мы рассмотрим полноценный пример использования этого паттерна.

Коротко о паттерне Посетитель

Посетитель — объект, предназначенный для выполнения обхода коллекций структур. Тип и способ хранения коллекции особого значения не имеет.

В простейшем случае Посетителю достаточно одной функции-члена visit(). Эта функция принимает данные, соответствующие каждому элементу коллекции. При этом для всех элементов может происходить та или иная обработка.

Результатом обхода Посетителя по коллекции становится новая структура. Она может представлять собой преобразование исходной коллекции в особый формат или содержать результаты анализа данных коллекции (например, статистический).

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

Возвращаемся к примеру простого компилятора

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

Мы предусмотрели возможность полиморфной генерации кода на C++ и Java с помощью Фабрик. Но каждый раз процесс генерации начинается с нуля. Это крайне неэффективно.

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

Работать с деревом синтаксического разбора будут конкретные Посетители. В нашем случае можно использовать CppVisitor и JavaVisitor. Нам понадобится всего по одному классу, вместо Фабрик и горы вспомогательных классов-элементов.

Рефакторинг части Компоновщика

Базовый класс Unit принимает следующий вид:

Интерфейс Unit не сильно изменился по сравнению с тем, что было. Исчезли функции compile() и generateShift(), но добавилась функция visit(). С убранными функциями все понятно. Теперь Unit предназначен исключительно для компоновки и хранения структуры, но не для ее обработки и преобразований.

Добавленная функция visit() принимает указатель на Посетителя Visitor и уровень level, которому соответствует вызов. В этой функции и будет происходить обслуживание Посетителя.

Сам класс Visitor определен, как вложенный в Unit. Обратите внимание на его интерфейс. Для элементов Класса и Метода предусмотрены пары функций-членов enter() и leave(). Это нужно для отслеживания начала и конца этих синтаксических структур, которые могут иметь вложенные элементы. Оператор вывода PrintOperatorUnit вложенных элементов иметь не может, поэтому для него такие ухищрения излишни.

Кстати, generateShift() переехал в Visitor. Теперь вопросы выравнивания целиком и полностью его проблемы.

Элемент Класс

Вот новая версия ClassUnit:

Изменений довольно много:

  1. Класс ClassUnit больше не абстрактный;
  2. Упрощена внутренняя структура хранения вложенных полей. Все добавляется в единый контейнер без разделения по уровням доступа;
  3. Появилась новая открытая функция-член getName(). Через нее Посетитель сможет узнать имя Класса.

Наибольший интерес для нас представляет функция ClassUnit::visit(). Сначала она сообщает посетителю о начале обхода путем вызова функции-члена Посетителя enter().

Затем в цикле происходит посещение каждого поля Класса. Для них запускается функция visit() с текущим посетителем в качестве первого аргумента.

В самом конце функции мы прощаемся с Посетителем. Говорим ему с помощью leave(), что обход окончен.

Элемент Метод

Следующим по порядку идет MethodUnit:

Для него мы добавили функции-геттеры getName() и getReturnType(). А также создали вспомогательную функцию getModifier(), которая на основе значения m_flags определяет область видимости.

Функция обхода visit() дословно повторяет то, что мы видели для ClassUnit. Уже можно задуматься над созданием вспомогательного базового класса или другим способом устранения дублирования кода. Но чтобы не увеличивать размер статьи, я этого делать не буду.

Элемент Оператор печати

Остался последний элемент:

Думаю, этот код не нуждается в пояснениях.

Использование нового Компоновщика

Мы уже можем заняться построением синтаксических деревьев. И все это без какой-либо привязки к какому-либо языку программирования:

Отлично. Just as planned! Осталось только реализовать Посетителей.

Java-Посетитель

В этот раз начнем с генератора кода на Java. Вот как он может выглядеть:

По сути, я просто перенес код из функций Unit::compile(), которые у нас получились в прошлый раз. Правда, теперь необходимые значения (имя класса, имя метода и т.д.) извлекаются из переданных элементов.

Также обратите внимание, что результат добавляется к переменной m_result. Извлечь его можно с помощью предусмотренной функции-геттера getResult().

Небольшой проблемой получившегося Посетителя является то, что он «одноразовый». Если мы запустим его второй раз, то старый результат останется на месте. А новый к нему «приклеится». Но устранить этот недостаток довольно легко. Достаточно обзавестись функцией Visitor::clear().

Воспользуемся нашим Java-Посетителем:

Результат работы программы следующий:

Все правильно. Можно двигаться дальше.

Cpp-Посетитель

Реализация этого Посетителя получилась чуть сложнее:

Издержки этого кода связаны с тем, что мы должны отслеживать момент перехода с одной области видимости внутри Класса к другой. Последнюю область видимости для каждого уровня вложенности мы храним в ассоциативном массиве m_modifiers. Если область видимости изменилась, то выводится соответствующий модификатор (public, protected или private);

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

Остальная часть кода, как и для Java-Посетителя, дословно повторяет то, что раньше было в функциях compile(). Все готово к пробному запуску:

На консоль будет выведено:

Выводы

Пример получился довольно длинным. Материал пришлось разбить на три части. В них мы посмотрели паттерны Компоновщик, Абстрактная фабрика и Посетитель. Все они полезны по своему. Имеют слабые и сильные стороны.

Для нашего конкретного случая более предпочтительным выглядит решение из комбинации Компоновщик+Посетитель. Оно дает существенное преимущество по сравнению с версией на основе Абстрактной фабрики. Это преимущество заключается в четком разделении логики формирования структуры дерева и его обработки.

Но из этого не следует, что об Абстрактной фабрике можно забыть. Это тоже полезный паттерн. И он имеет свои плюсы, о которых мы говорили в прошлой части.

Как обычно, выбор того или иного паттерна и той или иной архитектуры зависит от конкретных задач и особенностей приложения.

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