Тази статия се нуждае от подобрение.
Необходимо е: форматиране.Ако желаете да помогнете на Уикипедия, използвайте опцията редактиране в горното меню над статията, за да нанесете нужните корекции.
В обектно-ориентираното програмиране и софтуерното инженерство, дизайнерският шаблон, наречен „посетител“, представлява метод за отделяне на даден алгоритъм от обектната структура, върху която оперира. Практическата полза от подобно отделяне идва от възможността да се добавят нови свойства и операции към съществуващи вече структури без да се налага модификация. Заради свойствата си посетителският модел е един от начините да се следва отворено/затворения принцип.
По същество, посетителят дава възможност на потребител да добавя виртуални функции на семейство класове без да ги променя. За целта се създава посетителски клас, който имплементира всички, подходящи специализации на виртуалната функция. Посетителят приема за входящи данни референция на класа и постига желания резултат, чрез така нареченото двойно изпращане.
В своята същност Посетителят е идеалният шаблон за достъп до публични библиотеки, защото позволява изпълнението на операции върху класове чрез „посетителски“ клас, което от своя страна спестява преработката на оригиналния код.
Мотивация
Да вземем за пример разработването на 2D CAD система. В своето ядро има няколко типа, които представляват основните геометрични форми като кръгове, линии и дъги. Субектите са подредени на слоеве, като на върха на йерархията стои типа рисунка, който е просто списък на слоеве, плюс някои допълнителни свойства.
Основна работа на този вид йерархия е запазването на чертежа в основния формат на системата. На пръв поглед може да изглежда приемливо добавянето на локални методи за запис към всички типове в йерархията. Нуждата от запис на чертежите в други файлови формати, обаче, налага добавянето на още и още методи за запис и скоро настъпва хаос в иначе относително чистата геометрична структура на данните, с която сме започнали.
Наивно решение би била поддръжката на отделни функции за всеки файлов формат. Такава функция за запис ще приема рисунката като вход и ще я прекодира в конкретен файлов формат. Следването на подобна схема само за няколко различни формати, би довело до дублиране на функциите. Например, записването на кръг в растерен формат изисква подобен код (без значение каква конкретна растерна форма се използва), но различен за другите примитивни форми; Следователно кодът става голям външен цикъл, преминаващ през обектите с голямо разклонение от решения спрямо вида на обекта. Друг проблем при този подход е, че е много лесно да пропуснете форма в една или повече записващи функции, или е въведен нов примитивен тип, но записа се прилага само за един тип файл, а за другите не, което автоматично води до удължаване на кода и проблеми по поддръжката.
Вместо това, може да се прилага схемата Посетител. Посетителският шаблон кодира логическата операция на цялата йерархия в един клас, съдържащ един метод за всеки тип. В примера за системата CAD, всяка записваща функция ще бъде изпълнена като отделен Посетителски подклас. Това ще премахне дублирането на всички проверки за тип и ще накара компилаторът да се оплаче, ако формата е пропусната.
Друга мотивация е да се преизползва итериращ код. Например итерациите върху структура от директории могат да бъдат постигнати с посетителския модел. Това ще ви позволи да създадете файл-търсения, резервни копия на файлове, премахване на директории и т.н., чрез имплементацията на посетител за всяка функция, преизползвайки кода за итерация.
Детайли
Моделът Посетител изисква език за програмиране, който поддържа единично изпращане. При това условие, вземаме за пример два обекта, всеки от някакъв вид клас; единият се нарича „елемент“, а другият се нарича „посетител“. Елемент има метод accept(), който може да приема посетител като аргумент. Метода accept() извиква метода visit() на посетителя; елемент се изпраща като аргумент на метода на visit(). По този начин:
Когато accept() метода бъде извикан от програмата, неговата имплементация се избира въз основа на следните две условия:
Динамичният тип на елемента.
Статичният вид на посетителя.
Когато асоциираният visit() метод бъде извикан, неговото изпълнение се избира въз основа на следните две условия:
Динамичният вид на посетителя.
Статичният тип на елемента, който е в рамките на имплементацията на метода accept(), и който е еквивалентен на динамичния тип на елемента. (Като бонус, ако посетителят не може да се справи с аргумент от дадения тип елемент, тогава компилаторът ще хване грешката.)
Следователно, имплементацията на метода на accept() се избира въз основа на следните две условия:
Динамичният тип на елемента.
Динамичният вид на посетителя.
Това на практика изпълнява двойно изпращане и тъй като Common Lisp езика поддържа многократно изпращане (не само единично изпращане), прилагането на шаблона посетител в Lisp е тривиално.
По този начин един алгоритъм може да бъде написан за обхождане на графи от елементи, като едновременно с това да се извършват много други операции чрез подаване на различни видове посетители, които да взаимодействат с елементите базирани на динамичните типове, както на елементите така и на посетителите.
Java пример
Следният пример е от Java, и показва как съдържанието на дърво от възли (във този случай описващо компонентите на автомобил) може да бъде отпечатано. Вместо да създаваме „отпечатващ“ метод във всеки от подкласовете (Колело, Двигател, Тяло, и Автомобил), създаваме само един посетителски клас CarElementPrintVisitor, който да изпълнява отпечатването. Тъй като различните подкласове имат различни изисквания за отпечатването, CarElementPrintVisitor дефинира отпечатващи visit() методи с различен тип на аргумента. CarElementDoVisitor извършва други действия с компонентите на автомобила и дефинира различните visit() методи по подобен начин. CarElementPrintVisitor и CarElementDoVisitor се отнасят така, както се отнасят посетителите, които запазват графичните данни в различен файлов формат.
Диаграма
Източници
interfaceICarElementVisitor{voidvisit(Wheelwheel);voidvisit(Engineengine);voidvisit(Bodybody);voidvisit(Carcar);}interfaceICarElement{voidaccept(ICarElementVisitorvisitor);// CarElements have to provide accept().}classWheelimplementsICarElement{privateStringname;publicWheel(Stringname){this.name=name;}publicStringgetName(){returnthis.name;}publicvoidaccept(ICarElementVisitorvisitor){/* * accept(ICarElementVisitor) in Wheel implements * accept(ICarElementVisitor) in ICarElement, so the call * to accept is bound at run time. This can be considered * the first dispatch. However, the decision to call * visit(Wheel) (as opposed to visit(Engine) etc.) can be * made during compile time since 'this' is known at compile * time to be a Wheel. Moreover, each implementation of * ICarElementVisitor implements the visit(Wheel), which is * another decision that is made at run time. This can be * considered the second dispatch. */visitor.visit(this);}}classEngineimplementsICarElement{publicvoidaccept(ICarElementVisitorvisitor){visitor.visit(this);}}classBodyimplementsICarElement{publicvoidaccept(ICarElementVisitorvisitor){visitor.visit(this);}}classCarimplementsICarElement{ICarElement[]elements;publicCar(){//create new Array of elementsthis.elements=newICarElement[]{newWheel("front left"),newWheel("front right"),newWheel("back left"),newWheel("back right"),newBody(),newEngine()};}publicvoidaccept(ICarElementVisitorvisitor){for(ICarElementelem:elements){elem.accept(visitor);}visitor.visit(this);}}classCarElementPrintVisitorimplementsICarElementVisitor{publicvoidvisit(Wheelwheel){System.out.println("Visiting "+wheel.getName()+" wheel");}publicvoidvisit(Engineengine){System.out.println("Visiting engine");}publicvoidvisit(Bodybody){System.out.println("Visiting body");}publicvoidvisit(Carcar){System.out.println("Visiting car");}}classCarElementDoVisitorimplementsICarElementVisitor{publicvoidvisit(Wheelwheel){System.out.println("Kicking my "+wheel.getName()+" wheel");}publicvoidvisit(Engineengine){System.out.println("Starting my engine");}publicvoidvisit(Bodybody){System.out.println("Moving my body");}publicvoidvisit(Carcar){System.out.println("Starting my car");}}publicclassVisitorDemo{publicstaticvoidmain(String[]args){ICarElementcar=newCar();car.accept(newCarElementPrintVisitor());car.accept(newCarElementDoVisitor());}}
Резултат
Visiting front left wheel
Visiting front right wheel
Visiting back left wheel
Visiting back right wheel
Visiting body
Visiting engine
Visiting car
Kicking my front left wheel
Kicking my front right wheel
Kicking my back left wheel
Kicking my back right wheel
Moving my body
Starting my engine
Starting my car
Източници
(defclassauto((elements:initarg:elements)))(defclassauto-part((name:initarg:name:initform"<unnamed-car-part>")))(defmethodprint-object((pauto-part)stream)(print-object(slot-valuep'name)stream))(defclasswheel(auto-part))(defclassbody(auto-part))(defclassengine(auto-part))(defgenerictraverse(functionobjectother-object))(defmethodtraverse(function(aauto)other-object)(with-slots(elements)a(dolist(eelements)(funcallfunctioneother-object))));; do-something visitations;; catch all(defmethoddo-something(objectother-object)(formatt"don't know how ~s and ~s should interact~%"objectother-object));; visitation involving wheel and integer(defmethoddo-something((objectwheel)(other-objectinteger))(formatt"kicking wheel ~s ~s times~%"objectother-object));; visitation involving wheel and symbol(defmethoddo-something((objectwheel)(other-objectsymbol))(formatt"kicking wheel ~s symbolically using symbol ~s~%"objectother-object))(defmethoddo-something((objectengine)(other-objectinteger))(formatt"starting engine ~s ~s times~%"objectother-object))(defmethoddo-something((objectengine)(other-objectsymbol))(formatt"starting engine ~s symbolically using symbol ~s~%"objectother-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-right-wheel"),(make-instance'wheel:name"rear-right-wheel"),(make-instance'body:name"body"),(make-instance'engine:name"engine")))));; traverse to print elements;; stream *standard-output* plays the role of other-object here(traverse#'printa*standard-output*)(terpri);; print newline;; traverse with arbitrary context from other object(traverse#'do-somethinga42);; traverse with arbitrary context from other object(traverse#'do-somethinga'abc))
Резултат
„front-left-wheel“
„front-right-wheel“
„rear-right-wheel“
„rear-right-wheel“
„body“
„engine“
kicking wheel „front-left-wheel“ 42 times
kicking wheel „front-right-wheel“ 42 times
kicking wheel „rear-right-wheel“ 42 times
kicking wheel „rear-right-wheel“ 42 times
don't know how „body“ and 42 should interact
starting engine „engine“ 42 times
kicking wheel „front-left-wheel“ symbolically using symbol ABC
kicking wheel „front-right-wheel“ symbolically using symbol ABC
kicking wheel „rear-right-wheel“ symbolically using symbol ABC
kicking wheel „rear-right-wheel“ symbolically using symbol ABC
don't know how „body“ and ABC should interact
starting engine „engine“ symbolically using symbol ABC
Бележки
Другият
other-object е излишен в този
случай.
Причината е, че е възможно да
използваме анонимна функция която
извиква желания метод с лексикално
хванат обект:
(defmethodtraverse(function(aauto));; other-object removed(with-slots(elements)a(dolist(eelements)(funcallfunctione))));; from here too;; ...;; alternative way to print-traverse(traverse(lambda(o)(printo*standard-output*))a);; alternative way to do-something with;; elements of a and integer 42(traverse(lambda(o)(do-somethingo42))a)
Множественото
изпращане се случва при извикването
подадено от тялото на анонимната
функция, и обхождането и е мапваща функция
която разпределя приложението на
функцията върху елементите на обект.
По
този начин всички следи от посетителския
модел изчезват, освен мапващата функция, в
която няма следа че 2 обекта са били
използвани.
Всички
следи че има 2 обекта и изпращането на
техните типове е в ламбда функция.
Състояние
Като изключим потенциалното подобряване на разделението от опасения, посетителският модел има допълнително предимство пред това просто да извикаме полиморфичен метод:посетителският обект може да има състояние.
Това е изключително полезно в много случаи в които действието извършвано върху обект зависи от предишни действия.
Такъв принтиращ обект (имплементиран като посетител, в този случай), ще посети възлите в структура от данни които представляват парсната и обработена програма. Компилаторът ще принтира текстово представяне на програмното дърво. За да направи представянето четимо от хора, компилаторът трябва правилно да представи програмните конструкции и изрази.
Сегашното ниво на вдлъбнатина може да бъде следено от посетителят, както неговото състояние, така и коректно приложената енкапсулация, докато в обикновен полиморфичен метод призоваването и нивото на вдлъбнатост биха били прекалено открити като параметри и повиквателя ще разчита на методната имплементация да използва параметъра правилно.
Сродни модели на дизайн
Командващ модел: Подобно на посетителския модел той енкапсулира една или повече функции в обект, за да ги представи пред повиквателя. За разлика от посетителския модел, командващият модел не прилага принципа за обхождане на обектовата структура.
Итераторен модел: Този модел дефинира обхождащ принцип подобно на посетителския модел, без обаче да прави разлика между типовете в обхождания обект.