Шаблон посетителя — это шаблон проектирования программного обеспечения , который отделяет алгоритм от структуры объекта . Благодаря такому разделению новые операции могут быть добавлены к существующим структурам объектов без изменения структур. Это один из способов следовать принципу открытости/закрытости в объектно-ориентированном программировании и программной инженерии .
По сути, посетитель позволяет добавлять новые виртуальные функции в семейство классов , не изменяя классы. Вместо этого создается класс посетителя, который реализует все соответствующие специализации виртуальной функции. Посетитель принимает ссылку на экземпляр в качестве входных данных и реализует цель посредством двойной диспетчеризации .
Языки программирования с типами сумм и сопоставлением с образцом исключают многие преимущества шаблона посетителя, поскольку класс посетителя может как легко выполнять переход по типу объекта, так и генерировать ошибку компиляции, если определен новый тип объекта, который посетитель еще не обрабатывает.
Шаблон проектирования «Посетитель » [1] — один из двадцати трех известных шаблонов проектирования «Банды четырех» , которые описывают, как решать повторяющиеся проблемы проектирования для разработки гибкого и повторно используемого объектно-ориентированного программного обеспечения, то есть объектов, которые проще реализовывать, изменять, тестировать и повторно использовать.
Когда новые операции требуются часто, а структура объекта состоит из множества не связанных между собой классов, негибко добавлять новые подклассы каждый раз, когда требуется новая операция, поскольку «[..] распределение всех этих операций по различным классам узлов приводит к системе, которую трудно понимать, поддерживать и изменять». [1]
Это позволяет создавать новые операции независимо от классов структуры объекта путем добавления новых объектов-посетителей.
См. также класс UML и диаграмму последовательности ниже.
« Банда четырех» определяет Посетителя как:
Представление операции, которая должна быть выполнена над элементами структуры объекта. Visitor позволяет определить новую операцию, не изменяя классы элементов, над которыми она работает.
Природа Visitor делает его идеальным шаблоном для подключения к публичным API, позволяя клиентам выполнять операции с классом, используя «посещающий» класс, без необходимости изменять исходный код. [2]
Перемещение операций в классы посетителей выгодно, когда
Однако недостатком этого шаблона является то, что он затрудняет расширение иерархии классов, поскольку новые классы обычно требуют visit
добавления нового метода к каждому посетителю.
Рассмотрим проектирование системы 2D -компьютерного проектирования (САПР). В ее основе есть несколько типов для представления базовых геометрических фигур, таких как окружности, линии и дуги. Сущности упорядочены по слоям, а на вершине иерархии типов находится чертеж, который представляет собой просто список слоев, плюс некоторые добавленные свойства.
Фундаментальной операцией в этой иерархии типов является сохранение чертежа в собственном формате файла системы. На первый взгляд может показаться приемлемым добавлять локальные методы сохранения ко всем типам в иерархии. Но также полезно иметь возможность сохранять чертежи в других форматах файлов. Добавление все большего количества методов сохранения во множество различных форматов файлов вскоре загромождает относительно чистую исходную геометрическую структуру данных.
Наивным способом решения этой проблемы было бы сохранение отдельных функций для каждого формата файла. Такая функция сохранения принимала бы рисунок в качестве входных данных, обходила его и кодировала в этот конкретный формат файла. Поскольку это делается для каждого добавленного другого формата, дублирование между функциями накапливается. Например, сохранение формы круга в растровом формате требует очень похожего кода, независимо от того, какая конкретная растровая форма используется, и отличается от других примитивных форм. Случай с другими примитивными формами, такими как линии и многоугольники, аналогичен. Таким образом, код становится большим внешним циклом, проходящим через объекты, с большим деревом решений внутри цикла, запрашивающим тип объекта. Другая проблема с этим подходом заключается в том, что очень легко пропустить форму в одном или нескольких хранителях, или вводится новая примитивная форма, но процедура сохранения реализована только для одного типа файла, а не для других, что приводит к проблемам расширения кода и обслуживания. По мере роста версий одного и того же файла его становится сложнее поддерживать.
Вместо этого можно применить шаблон visitor. Он кодирует логическую операцию (т. е. save(image_tree)) по всей иерархии в один класс (т. е. Saver), который реализует общие методы обхода дерева и описывает виртуальные вспомогательные методы (т. е. save_circle, save_square и т. д.), которые должны быть реализованы для форматно-специфических поведений. В случае примера CAD такие форматно-специфические поведения будут реализованы подклассом Visitor (т. е. SaverPNG). Таким образом, все дублирование проверок типов и шагов обхода удаляется. Кроме того, компилятор теперь жалуется, если пропущена форма, поскольку теперь она ожидается общей базовой функцией обхода/сохранения.
Шаблон посетителя может использоваться для итерации по структурам данных, подобным контейнерам , как и шаблон Итератора , но с ограниченной функциональностью. [3] : 288 Например, итерация по структуре каталога может быть реализована классом функций вместо более обычного шаблона цикла . Это позволило бы извлекать различную полезную информацию из содержимого каталогов, реализуя функциональность посетителя для каждого элемента при повторном использовании кода итерации. Он широко используется в системах Smalltalk и может быть найден также в C++. [3] : 289 Однако недостатком этого подхода является то, что вы не можете легко выйти из цикла или выполнять итерацию одновременно (параллельно, т. е. проходя по двум контейнерам одновременно с помощью одной i
переменной). [3] : 289 Последнее потребовало бы написания дополнительной функциональности для посетителя для поддержки этих функций. [3] : 289
В приведенной выше диаграмме классов UML класс не реализует новую операцию напрямую. Вместо этого реализует операцию диспетчеризации , которая «диспетчеризует» (делегирует) запрос «принятому объекту посетителя» ( ). Класс реализует операцию ( ). затем реализует путем диспетчеризации в . Класс реализует операцию ( ).ElementA
ElementA
accept(visitor)
visitor.visitElementA(this)
Visitor1
visitElementA(e:ElementA)
ElementB
accept(visitor)
visitor.visitElementB(this)
Visitor1
visitElementB(e:ElementB)
Диаграмма последовательности UML
показывает взаимодействия во время выполнения: Объект проходит по элементам структуры объекта ( ) и вызывает каждый элемент.
Сначала вызывается , который вызывает принятый объект. Сам элемент ( ) передается в , чтобы он мог «посетить» (вызвать ).
После этого вызываются , который вызывает , который « посещает» (вызывает ).Client
ElementA,ElementB
accept(visitor)
Client
accept(visitor)
ElementA
visitElementA(this)
visitor
this
visitor
ElementA
operationA()
Client
accept(visitor)
ElementB
visitElementB(this)
visitor
ElementB
operationB()
Шаблон visitor требует языка программирования , который поддерживает одиночную диспетчеризацию , как это делают обычные объектно-ориентированные языки (такие как C++ , Java , Smalltalk , Objective-C , Swift , JavaScript , Python и C# ). При этом условии рассмотрим два объекта, каждый из которых принадлежит некоторому типу класса; один называется element , а другой — visitor .
Посетитель объявляет метод, который принимает элемент в качестве аргумента, для каждого класса элементов. Конкретные посетители выводятся из класса посетителя и реализуют эти методы, каждый из которых реализует часть алгоритма, работающего со структурой объекта. Состояние алгоритма поддерживается локально конкретным классом посетителя.visit
visit
Элемент объявляет метод для принятия посетителя, принимая посетителя в качестве аргумента. Конкретные элементы , полученные из класса элемента, реализуют метод. В простейшей форме это не более чем вызов метода посетителя . Составные элементы, которые поддерживают список дочерних объектов, обычно перебирают их , вызывая метод каждого дочернего объекта .accept
accept
visit
accept
Клиент создает структуру объекта, напрямую или косвенно, и создает экземпляры конкретных посетителей. Когда должна быть выполнена операция, которая реализуется с использованием шаблона Посетитель, он вызывает методaccept
элемента(ов) верхнего уровня.
Когда accept
метод вызывается в программе, его реализация выбирается на основе как динамического типа элемента, так и статического типа посетителя. Когда visit
вызывается связанный метод, его реализация выбирается на основе как динамического типа посетителя, так и статического типа элемента, как известно из реализации метода accept
, который совпадает с динамическим типом элемента. (В качестве бонуса, если посетитель не может обработать аргумент типа данного элемента, то компилятор поймает ошибку.)
Таким образом, реализация метода visit
выбирается на основе как динамического типа элемента, так и динамического типа посетителя. Это эффективно реализует двойную диспетчеризацию . Для языков, объектные системы которых поддерживают множественную диспетчеризацию, а не только одиночную, таких как Common Lisp или C# через Dynamic Language Runtime (DLR), реализация шаблона посетителя значительно упрощается (он же Dynamic Visitor), позволяя использовать простую перегрузку функций для охвата всех посещаемых случаев. Динамический посетитель, при условии, что он работает только с общедоступными данными, соответствует принципу открытости/закрытости (поскольку он не изменяет существующие структуры) и принципу единой ответственности (поскольку он реализует шаблон Visitor в отдельном компоненте).
Таким образом, можно написать один алгоритм для обхода графа элементов, и во время этого обхода можно выполнять множество различных видов операций, предоставляя различные виды посетителей для взаимодействия с элементами на основе динамических типов как элементов, так и посетителей.
В этом примере объявляется отдельный ExpressionPrintingVisitor
класс, который отвечает за печать. Если требуется введение нового конкретного посетителя, будет создан новый класс для реализации интерфейса Visitor, а также будут предоставлены новые реализации для методов Visit. Существующие классы (Literal и Addition) останутся без изменений.
с использованием Системы ; пространство имен Википедия ; открытый интерфейс Посетитель { void Visit ( Литерал литерал ); void Visit ( Дополнение дополнение ); } public class ExpressionPrintingVisitor : Visitor { public void Visit ( Literal literal ) { Console . WriteLine ( literal . Value ); } public void Visit ( Addition addition ) { double leftValue = addition.Left.GetValue ( ) ; double rightValue = addition.Right.GetValue ( ) ; var sum = addition.GetValue ( ) ; Console.WriteLine ( " {0} + {1} = {2} " , leftValue , rightValue , sum ) ; } } public abstract class Expression { public abstract void Accept ( Visitor v ); public abstract double GetValue (); } public class Literal : Expression { public Literal ( double value ) { this.Value = value ; } публичное двойное значение { получить ; установить ; } public override void Accept ( Visitor v ) { v.Visit ( this ) ; } public override double GetValue ( ) { return Value ; } } public class Addition : Expression { public Addition ( Expression left , Expression right ) { Left = left ; Right = right ; } публичное выражение слева { получить ; установить ; } публичное выражение справа { получить ; установить ; } public override void Accept ( Visitor v ) { Left.Accept ( v ) ; Right.Accept ( v ) ; v.Visit ( this ) ; } public override double GetValue ( ) { return Left.GetValue ( ) + Right.GetValue ( ) ; } } public static class Program { public static void Main ( string [] args ) { // Эмуляция 1 + 2 + 3 var e = new Addition ( new Addition ( new Literal ( 1 ), new Literal ( 2 ) ), new Literal ( 3 ) ); var printingVisitor = new ExpressionPrintingVisitor ( ); e.Accept ( printingVisitor ) ; Console.ReadKey ( ) ; } }
В этом случае, это обязанность объекта знать, как печатать себя в потоке. Посетителем здесь является объект, а не поток.
"Синтаксиса для создания класса нет. Классы создаются путем отправки сообщений другим классам." WriteStream подкласс: #ExpressionPrinter instanceVariableNames: '' classVariableNames: '' package: 'Wikipedia' .ExpressionPrinter >>write: anObject "Делегирует действие объекту. Объект не обязательно должен относиться к какому-либо специальному классу; он должен только уметь понимать сообщение #putOn:" anObject putOn: self . ^ anObject .Подкласс объекта : #Expression instanceVariableNames: '' classVariableNames: '' package: 'Wikipedia' .Подкласс выражения : #Литеральный экземплярVariableNames: 'value' classVariableNames: '' package: 'Wikipedia' .Класс Literal >>with: aValue "Метод класса для построения экземпляра класса Literal" ^ self new value: aValue ; yourself .Литерал >>value: aValue "Сеттер для значения" value := aValue .Literal >>putOn: aStream "Объект Literal знает, как печатать себя" aStream nextPutAll: value asString .Подкласс выражения : #Добавление instanceVariableNames: 'left right' classVariableNames: '' package: 'Wikipedia' .Класс сложения >>слева: a справа: b "Метод класса для построения экземпляра класса сложения" ^ self new слева: a ; справа: b ; сами .Добавление >>left: anExpression "Установщик для left" left := anExpression .Дополнение >>right: anExpression "Установщик для right" right := anExpression .Addition >>putOn: aStream "Объект Addition знает, как печатать себя" aStream nextPut: $( . left putOn: aStream . aStream nextPut: $+ . right putOn: aStream . aStream nextPut: $) .Подкласс объекта : #Program instanceVariableNames: '' classVariableNames: '' package: 'Wikipedia' .Программа >> main | expression stream | expression := Сложение слева: ( Сложение слева: ( Литерал с: 1 ) справа: ( Литерал с: 2 )) справа: ( Литерал с: 3 ) . stream := ExpressionPrinter on: ( Строка new: 100 ) . stream write: expression . Transcript show: содержимое потока . Transcript flush .
Go не поддерживает перегрузку методов, поэтому методы посещения должны иметь разные имена. Типичный интерфейс посетителя может быть
тип Интерфейс посетителя { visitWheel ( колесо Колесо ) строка visitEngine ( двигатель Двигатель ) строка visitBody ( тело Тело ) строка visitCar ( автомобиль Автомобиль ) строка }
Следующий пример написан на языке Java и показывает, как можно распечатать содержимое дерева узлов (в данном случае описывающих компоненты автомобиля). Вместо создания print
методов для каждого подкласса узла ( Wheel
, Engine
, Body
, и Car
), один класс посетителя ( CarElementPrintVisitor
) выполняет требуемое действие печати. Поскольку для разных подклассов узлов требуются немного разные действия для правильной печати, CarElementPrintVisitor
отправляет действия на основе класса аргумента, переданного его visit
методу. CarElementDoVisitor
, который аналогичен операции сохранения для другого формата файла, делает то же самое.
импорт java.util.List ; интерфейс CarElement { void accept ( CarElementVisitor посетитель ); } интерфейс CarElementVisitor { void visit ( Кузов кузов ) ; void visit ( Автомобиль автомобиль ); void visit ( Двигатель двигатель ); void visit ( Колесо колесо ); } класс Колесо реализует CarElement { private final String name ; public Wheel ( final String name ) { this.name = name ; } public String getName () { return name ; } @Override public void accept ( CarElementVisitor visitor ) { /* * accept(CarElementVisitor) в Wheel реализует * accept(CarElementVisitor) в CarElement, поэтому вызов * accept привязан во время выполнения. Это можно считать * *первой* отправкой. Однако решение о вызове * visit(Wheel) (в отличие от visit(Engine) и т. д.) может быть * принято во время компиляции, так как во время компиляции * известно, что 'this' является Wheel. Более того, каждая реализация * CarElementVisitor реализует visit(Wheel), что является * еще одним решением, принимаемым во время выполнения. Это можно * считать *второй* отправкой. */ visitor . visit ( this ); } } класс Body реализует CarElement { @Override public void accept ( CarElementVisitor visitor ) { visitor . visit ( this ); } } class Engine реализует CarElement { @Override public void accept ( CarElementVisitor visitor ) { visitor . visit ( this ); } } класс Car реализует CarElement { private final Список элементов < CarElement > ; public Car () { this . elements = List . of ( new Wheel ( "переднее левое" ), new Wheel ( "переднее правое" ), new Wheel ( "заднее левое" ), new Wheel ( "заднее правое" ), new Body (), new Engine () ); } @Override public void accept ( CarElementVisitor visitor ) { for ( CarElement element : elements ) { element.accept ( visitor ) ; } visitor.visit ( this ) ; } } class CarElementDoVisitor implements CarElementVisitor { @Override public void visit ( Body body ) { System . out . println ( "Перемещение моего тела" ); } @Override public void visit ( Car car ) { System.out.println ( " Завожу машину " ) ; } @Override public void visit ( Wheel wheel ) { System.out.println ( " Пинаю свой " + wheel.getName ( ) + " wheel " ) ; } @Override public void visit ( Engine engine ) { System . out . println ( "Запускаю двигатель" ); } } class CarElementPrintVisitor реализует CarElementVisitor { @Override public void visit ( Body body ) { System . out . println ( "Посещение body" ); } @Override public void visit ( Car car ) { System.out.println ( " Посещение car " ) ; } @Override public void visit ( Engine engine ) { System.out.println ( " Посещение engine " ) ; } @Override public void visit ( Wheel wheel ) { System.out.println ( " Посещение " + wheel.getName ( ) + " wheel " ) ; } } public class VisitorDemo { public static void main ( final String [] args ) { Car car = new Car (); автомобиль . принять ( новый CarElementPrintVisitor ()); автомобиль . принять ( новый CarElementDoVisitor ()); } }
Посещение переднего левого колесаПосещение переднего правого колесаПосещение заднего левого колесаПосещение заднего правого колесаПосещение телаВизит двигателяВизитная машинаПинаю переднее левое колесоПинаю переднее правое колесоПинаю заднее левое колесоПинаю заднее правое колесоДвигаю теломЗапускаю двигательЗавожу машину
( defclass auto () (( элементы : initarg : elements ))) ( defclass auto-part () (( name :initarg :name :initform "<unnamed-car-part>" ))) ( defmethod print-object ( ( p auto-part ) stream ) ( print-object ( slot-value p 'name ) stream )) ( defclass колесо ( auto-part ) ()) ( defclass body ( auto-part ) ()) ( двигатель defclass ( автозапчасть ) ()) ( defgeneric traverse ( function object other-object )) ( defmethod traverse ( function ( a auto ) other-object ) ( with-slots ( elements ) a ( dolist ( e elements ) ( funcall function e other-object )))) ;; посещения с целью что-то сделать;; перехватить все ( defmethod do-something ( object other-object ) ( format t "не знаю, как ~s и ~s должны взаимодействовать~%" object other-object )) ;; посещение с участием колеса и целого числа ( defmethod do-something (( object wheel ) ( other-object integer )) ( format t "пинание колеса ~s ~s раз~%" object other-object )) ;; посещение с участием колеса и символа ( defmethod do-something (( object wheel ) ( other-object symbol )) ( format t "пинание колеса ~s символически с использованием символа ~s~%" object other-object )) ( defmethod do-something (( object engine ) ( other-object integer )) ( format t "запуск двигателя ~s ~s раз~%" object other-object )) ( defmethod do-something (( object engine ) ( other-object symbol )) ( format t "запуск engine ~s символически с использованием символа ~s~%" object other-object )) ( let (( a ( make-instance 'auto :elements ` ( , ( make-instance 'wheel :name "front-left-wheel" ) , ( make-instance 'wheel :name "front-right-wheel" ) , ( make-instance 'wheel :name "rear-left-wheel" ) , ( make-instance 'wheel :name "rear-right-wheel" ) , ( make-instance 'body :name "body" ) , ( make-instance 'engine :name "engine" ))))) ;; переход к выводу элементов ;; поток *standard-output* здесь играет роль другого объекта ( traverse #' print a *standard-output* ) ( terpri ) ;; распечатать новую строку ;; обход с произвольным контекстом из другого объекта ( обход #' do-something a 42 ) ;; обход с произвольным контекстом из другого объекта ( обход #' do-something a 'abc ))
"переднее левое колесо""переднее правое колесо""заднее левое колесо""заднее правое колесо""тело""двигатель"удар ногой по колесу "переднее-левое-колесо" 42 разаудар ногой по колесу "переднее-правое-колесо" 42 разаудар ногой по колесу "заднее-левое-колесо" 42 разаудар ногой по колесу "заднее-правое-колесо" 42 разане знаю, как "тело" и 42 должны взаимодействоватьзапуск двигателя "двигатель" 42 разапиная колесо "переднее левое колесо" символически используя символ ABCпиная колесо "переднее-правое-колесо" символически используя символ ABCпиная колесо "заднее-левое-колесо" символически используя символ ABCпиная колесо "заднее-правое-колесо" символически используя символ ABCне знаю, как «тело» и ABC должны взаимодействоватьзапуск двигателя «двигатель» символически с использованием символа ABC
Параметр other-object
является излишним в traverse
. Причина в том, что можно использовать анонимную функцию, которая вызывает нужный целевой метод с лексически захваченным объектом:
( defmethod traverse ( function ( a auto )) ;; other-object removed ( with-slots ( elements ) a ( dolist ( e elements ) ( funcall function e )))) ;; отсюда тоже ;; ... ;; альтернативный способ print-traverse ( traverse ( lambda ( o ) ( print o *standard-output* )) a ) ;; альтернативный способ сделать что-то с ;; элементами a и целым числом 42 ( traverse ( lambda ( o ) ( do-something o 42 )) a )
Теперь множественная диспетчеризация происходит в вызове, выданном из тела анонимной функции, и поэтому traverse
является просто функцией отображения, которая распределяет применение функции по элементам объекта. Таким образом, все следы шаблона Visitor исчезают, за исключением функции отображения, в которой нет никаких доказательств участия двух объектов. Все знания о наличии двух объектов и диспетчеризации по их типам находятся в лямбда-функции.
Python не поддерживает перегрузку методов в классическом смысле (полиморфное поведение в зависимости от типа переданных параметров), поэтому методы «visit» для разных типов моделей должны иметь разные имена.
""" Пример шаблона посетителя. """из abc импорт ABCMeta , абстрактный методNOT_IMPLEMENTED = "Вам следует это реализовать."класс CarElement ( метакласс = ABCMeta ): @abstractmethod def accept ( self , visitor ): raise NotImplementedError ( NOT_IMPLEMENTED )класс Body ( CarElement ): def accept ( self , visitor ): visitor . visitBody ( self )класс Двигатель ( CarElement ): def accept ( self , visitor ): посетитель . visitEngine ( self )class Wheel ( CarElement ): def __init__ ( self , name ): self . name = name def accept ( self , visitor ): visitor . visitWheel ( self )class Car ( CarElement ): def __init__ ( self ): self . elements = [ Wheel ( "переднее левое" ), Wheel ( "переднее правое" ), Wheel ( "заднее левое" ), Wheel ( "заднее правое" ), Body (), Engine () ] def accept ( self , visitor ): для элемента in self . elements : element . accept ( visitor ) visitor . visitCar ( self )класс CarElementVisitor ( метакласс = ABCMeta ): @abstractmethod def visitBody ( self , element ) : вызвать NotImplementedError ( NOT_IMPLEMENTED ) @abstractmethod def visitEngine ( self , element ): вызвать NotImplementedError ( NOT_IMPLEMENTED ) @abstractmethod def visitWheel ( self , element ): вызвать NotImplementedError ( NOT_IMPLEMENTED ) @abstractmethod def visitCar ( self , element ): вызвать NotImplementedError ( NOT_IMPLEMENTED )class CarElementDoVisitor ( CarElementVisitor ): def visitBody ( self , body ): print ( "Движу телом." ) def visitCar ( self , car ): print ( "Завожу машину." ) def visitWheel ( self , wheel ): print ( "Пинаю колесо {} ." . format ( wheel . name )) def visitEngine ( self , engine ): print ( "Завожу двигатель." )class CarElementPrintVisitor ( CarElementVisitor ): def visitBody ( self , body ): print ( "Посещение body." ) def visitCar ( self , car ): print ( "Посещение car." ) def visitWheel ( self , wheel ): print ( "Посещение {} wheel." . format ( wheel . name )) def visitEngine ( self , engine ): print ( "Посещение engine." )автомобиль = Автомобиль () автомобиль . принять ( CarElementPrintVisitor ()) автомобиль . принять ( CarElementDoVisitor ())
Посещение переднего левого колеса. Посещение переднего правого колеса. Посещение заднего левого колеса. Посещение заднего правого колеса. Посещение тела. Посещение двигателя. Посещение автомобиля. Пинаю свое переднее левое колесо. Пинаю свое переднее правое колесо. Пинаю свое заднее левое колесо. Пинаю свое заднее правое колесо. Движу своим телом. Завожу свой двигатель. Завожу свою машину.
Использование Python 3 или выше позволяет реализовать общую реализацию метода accept:
класс Visitable : def accept ( self , visitor ): lookup = "visit_" + self .__ qualname__ . replace ( "." , "_" ) return getattr ( visitor , lookup )( self )
Можно расширить это, чтобы перебрать порядок разрешения методов класса, если они хотят вернуться к уже реализованным классам. Они также могут использовать функцию подкласса hook, чтобы определить поиск заранее.
{{cite book}}
: CS1 maint: несколько имен: список авторов ( ссылка )