Введение

Модель проектирования программного обеспечения. Шаблон "Посетитель" — это модель проектирования программного обеспечения, которая отделяет алгоритм от структуры объекта. Благодаря этому разделению новые операции можно добавлять к существующим структурам объектов, не изменяя сами структуры. Это один из способов реализации принципа открытости/закрытости в объектно-ориентированном программировании и разработке программного обеспечения. По сути, шаблон "Посетитель" позволяет добавлять новые виртуальные функции к семейству классов, не модифицируя эти классы. Вместо этого создается класс "Посетитель", который реализует все необходимые специализации виртуальной функции. "Посетитель" принимает ссылку на экземпляр объекта в качестве входных данных и выполняет целевую задачу посредством двойной диспетчеризации. Языки программирования, поддерживающие суммарные типы и сопоставление с образцом, нивелируют многие преимущества шаблона "Посетитель", поскольку класс "Посетитель" может легко выполнять ветвление в зависимости от типа объекта и генерировать ошибку компиляции при определении нового типа объекта, который он еще не обрабатывает.

Применение

Рассмотрим разработку 2D системы автоматизированного проектирования (САПР). В её основе лежат несколько типов для представления базовых геометрических фигур, таких как окружности, линии и дуги. Эти объекты упорядочены по слоям, а в верхней части иерархии типов находится чертеж, который представляет собой просто список слоев, дополненный некоторыми свойствами. Фундаментальной операцией над этой иерархией типов является сохранение чертежа в собственный формат файла системы. На первый взгляд, может показаться допустимым добавлять локальные методы сохранения ко всем типам в иерархии. Однако также полезно иметь возможность сохранять чертежи в другие форматы файлов. Добавление все большего числа методов сохранения для различных форматов файлов вскоре приведет к засорению относительно чистой исходной геометрической структуры данных. Наивным решением было бы поддерживать отдельные функции для каждого формата файла. Такая функция сохранения принимала бы чертеж в качестве входных данных, обходила бы его и кодировала в конкретный формат файла. По мере добавления новых форматов, дублирование кода между функциями будет накапливаться. Например, сохранение окружности в растровом формате требует очень похожего кода, независимо от конкретного используемого растрового формата, и отличается от сохранения других примитивных фигур. Аналогичная ситуация наблюдается и с другими примитивными фигурами, такими как линии и многоугольники. В результате код превращается в большой внешний цикл, проходящий по объектам, с большим деревом решений внутри цикла, запрашивающим тип объекта. Другая проблема этого подхода заключается в том, что легко пропустить фигуру при сохранении в одном или нескольких форматах, или ввести новую примитивную фигуру, но реализовать процедуру сохранения только для одного типа файла, а не для других, что приводит к проблемам расширения и поддержки кода. По мере роста версий одного и того же файла его поддержка становится все более сложной. Вместо этого можно применить шаблон "Посетитель". Он кодирует логическую операцию (например, save(дерево изображения)) над всей иерархией в один класс (например, Saver), который реализует общие методы для обхода дерева и определяет виртуальные вспомогательные методы (например, saveCircle, saveSquare и т.д.) для реализации специфичного для формата поведения. В случае примера с САПР, такое специфичное поведение формата будет реализовано подклассом "Посетителя" (например, SaverPNG). Таким образом, все дублирование проверок типа и этапов обхода устраняется. Кроме того, компилятор теперь будет выдавать ошибку, если фигура будет пропущена, поскольку она ожидается общей базовой функцией обхода/сохранения.

Итерационные петли

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

Подробности

Для шаблона посетителей требуется язык программирования, поддерживающий одинарную диспетчеризацию, как это делают распространенные объектно-ориентированные языки (такие как C++, Java, Smalltalk, Objective C, Swift, JavaScript, Python и C#). При этом условии рассмотрим два объекта, каждый из которых принадлежит к определенному типу класса: один называется элементом, а другой – посетителем. Посетитель объявляет метод `visit`, который принимает элемент в качестве аргумента для каждого класса элементов. Конкретные посетители наследуются от класса посетителя и реализуют эти методы `visit`, каждый из которых реализует часть алгоритма, работающего со структурой объектов. Состояние алгоритма поддерживается локально конкретным классом посетителя. Элемент объявляет метод `accept` для приема посетителя, принимая посетителя в качестве аргумента. Конкретные элементы, унаследованные от класса элемента, реализуют метод `accept`. В простейшем случае это не более чем вызов метода `visit` посетителя. Композитные элементы, содержащие список дочерних объектов, обычно перебирают их, вызывая метод `accept` каждого дочернего элемента. Клиент создает структуру объектов напрямую или косвенно и создает экземпляры конкретных посетителей. Когда необходимо выполнить операцию, реализованную с использованием шаблона посетителя, он вызывает метод `accept` элемента верхнего уровня (или элементов). Когда метод `accept` вызывается в программе, его реализация выбирается на основе как динамического типа элемента, так и статического типа посетителя. Когда вызывается соответствующий метод `visit`, его реализация выбирается на основе динамического типа посетителя и статического типа элемента, как известно из реализации метода `accept`, который соответствует динамическому типу элемента. (В качестве бонуса, если посетитель не может обработать аргумент типа данного элемента, компилятор обнаружит ошибку.) Таким образом, реализация метода `visit` выбирается на основе динамического типа элемента и динамического типа посетителя. Это фактически реализует двойную диспетчеризацию. Для языков, объектные системы которых поддерживают множественную диспетчеризацию, а не только одинарную, таких как Common Lisp или C# через Dynamic Language Runtime (DLR), реализация шаблона посетителя значительно упрощается (также известная как динамический посетитель) за счет использования простого переопределения функций для охвата всех посещаемых случаев. Динамический посетитель, при условии работы только с публичными данными, соответствует принципу открытости/закрытости (поскольку он не изменяет существующие структуры) и принципу единственной ответственности (поскольку он реализует шаблон посетителя в отдельном компоненте). Таким образом, один алгоритм может быть написан для обхода графа элементов, и во время этого обхода можно выполнить множество различных операций, предоставляя различные типы посетителей для взаимодействия с элементами на основе динамических типов как элементов, так и посетителей.

Пример Java

Следующий пример написан на языке Java и демонстрирует, как можно вывести содержимое дерева узлов (в данном случае, описывающего компоненты автомобиля). Вместо создания методов печати для каждого подкласса узлов (колесо, двигатель, кузов и автомобиль), один класс-посетитель (CarElementPrintVisitor) выполняет необходимое действие по печати. Поскольку для корректной печати различным подклассам узлов требуются немного разные действия, CarElementPrintVisitor распределяет действия в зависимости от класса аргумента, переданного в метод visit. CarElementDoVisitor, аналогичный операции сохранения в другой формат файла, действует аналогичным образом.

Пример Python

Python не поддерживает перегрузку методов в классическом смысле (полиморфное поведение в зависимости от типа переданных параметров), поэтому методы "визита" для различных типов моделей должны иметь разные имена.

Выпуск

Осматриваю переднее левое колесо. Осматриваю переднее правое колесо. Осматриваю заднее левое колесо. Осматриваю заднее правое колесо. Осматриваю кузов. Осматриваю двигатель. Осматриваю автомобиль. Пинаю переднее левое колесо. Пинаю переднее правое колесо. Пинаю заднее левое колесо. Пинаю заднее правое колесо. Двигаю кузовом. Завожу двигатель. Завожу автомобиль.