Направо към съдържанието

Посетител (шаблон)

от Уикипедия, свободната енциклопедия
UML диаграма на шаблона Посетител

В обектно-ориентираното програмиране и софтуерното инженерство, дизайнерският шаблон, наречен „посетител“, представлява метод за отделяне на даден алгоритъм от обектната структура, върху която оперира. Практическата полза от подобно отделяне идва от възможността да се добавят нови свойства и операции към съществуващи вече структури без да се налага модификация. Заради свойствата си посетителският модел е един от начините да се следва отворено/затворения принцип.

Visitor in LePUS3 (legend)

По същество, посетителят дава възможност на потребител да добавя виртуални функции на семейство класове без да ги променя. За целта се създава посетителски клас, който имплементира всички, подходящи специализации на виртуалната функция. Посетителят приема за входящи данни референция на класа и постига желания резултат, чрез така нареченото двойно изпращане.

Авторите на „Design Patterns – Шаблони за дизайн: Елементи на обектно-ориентирания софтуер за многократно използване“ определят Посетителят като: „Операция, която да бъде изпълнена върху елементи на обектна структура. Посетителят дава възможност за дефинирането на нови операции без да се модифицират класовете от елементи върху които оперира.

В своята същност Посетителят е идеалният шаблон за достъп до публични библиотеки, защото позволява изпълнението на операции върху класове чрез „посетителски“ клас, което от своя страна спестява преработката на оригиналния код.

Да вземем за пример разработването на 2D CAD система. В своето ядро има няколко типа, които представляват основните геометрични форми като кръгове, линии и дъги. Субектите са подредени на слоеве, като на върха на йерархията стои типа рисунка, който е просто списък на слоеве, плюс някои допълнителни свойства.

Основна работа на този вид йерархия е запазването на чертежа в основния формат на системата. На пръв поглед може да изглежда приемливо добавянето на локални методи за запис към всички типове в йерархията. Нуждата от запис на чертежите в други файлови формати, обаче, налага добавянето на още и още методи за запис и скоро настъпва хаос в иначе относително чистата геометрична структура на данните, с която сме започнали.

Наивно решение би била поддръжката на отделни функции за всеки файлов формат. Такава функция за запис ще приема рисунката като вход и ще я прекодира в конкретен файлов формат. Следването на подобна схема само за няколко различни формати, би довело до дублиране на функциите. Например, записването на кръг в растерен формат изисква подобен код (без значение каква конкретна растерна форма се използва), но различен за другите примитивни форми; Следователно кодът става голям външен цикъл, преминаващ през обектите с голямо разклонение от решения спрямо вида на обекта. Друг проблем при този подход е, че е много лесно да пропуснете форма в една или повече записващи функции, или е въведен нов примитивен тип, но записа се прилага само за един тип файл, а за другите не, което автоматично води до удължаване на кода и проблеми по поддръжката.

Вместо това, може да се прилага схемата Посетител. Посетителският шаблон кодира логическата операция на цялата йерархия в един клас, съдържащ един метод за всеки тип. В примера за системата CAD, всяка записваща функция ще бъде изпълнена като отделен Посетителски подклас. Това ще премахне дублирането на всички проверки за тип и ще накара компилаторът да се оплаче, ако формата е пропусната.

Друга мотивация е да се преизползва итериращ код. Например итерациите върху структура от директории могат да бъдат постигнати с посетителския модел. Това ще ви позволи да създадете файл-търсения, резервни копия на файлове, премахване на директории и т.н., чрез имплементацията на посетител за всяка функция, преизползвайки кода за итерация.

Моделът Посетител изисква език за програмиране, който поддържа единично изпращане. При това условие, вземаме за пример два обекта, всеки от някакъв вид клас; единият се нарича „елемент“, а другият се нарича „посетител“. Елемент има метод accept(), който може да приема посетител като аргумент. Метода accept() извиква метода visit() на посетителя; елемент се изпраща като аргумент на метода на visit(). По този начин:

  • Когато accept() метода бъде извикан от програмата, неговата имплементация се избира въз основа на следните две условия:
    • Динамичният тип на елемента.
    • Статичният вид на посетителя.
  • Когато асоциираният visit() метод бъде извикан, неговото изпълнение се избира въз основа на следните две условия:
    • Динамичният вид на посетителя.
    • Статичният тип на елемента, който е в рамките на имплементацията на метода accept(), и който е еквивалентен на динамичния тип на елемента. (Като бонус, ако посетителят не може да се справи с аргумент от дадения тип елемент, тогава компилаторът ще хване грешката.)
  • Следователно, имплементацията на метода на accept() се избира въз основа на следните две условия:
    • Динамичният тип на елемента.
    • Динамичният вид на посетителя.

Това на практика изпълнява двойно изпращане и тъй като Common Lisp езика поддържа многократно изпращане (не само единично изпращане), прилагането на шаблона посетител в Lisp е тривиално.

По този начин един алгоритъм може да бъде написан за обхождане на графи от елементи, като едновременно с това да се извършват много други операции чрез подаване на различни видове посетители, които да взаимодействат с елементите базирани на динамичните типове, както на елементите така и на посетителите.

Следният пример е от Java, и показва как съдържанието на дърво от възли (във този случай описващо компонентите на автомобил) може да бъде отпечатано. Вместо да създаваме „отпечатващ“ метод във всеки от подкласовете (Колело, Двигател, Тяло, и Автомобил), създаваме само един посетителски клас CarElementPrintVisitor, който да изпълнява отпечатването. Тъй като различните подкласове имат различни изисквания за отпечатването, CarElementPrintVisitor дефинира отпечатващи visit() методи с различен тип на аргумента. CarElementDoVisitor извършва други действия с компонентите на автомобила и дефинира различните visit() методи по подобен начин. CarElementPrintVisitor и CarElementDoVisitor се отнасят така, както се отнасят посетителите, които запазват графичните данни в различен файлов формат.

interface ICarElementVisitor {
 void visit(Wheel wheel);
 void visit(Engine engine);
 void visit(Body body);
 void visit(Car car);
}

interface ICarElement {
 void accept(ICarElementVisitor visitor); // CarElements have to provide accept().
}

class Wheel implements ICarElement {
 private String name;

 public Wheel(String name) {
 this.name = name;
 }

 public String getName() {
 return this.name;
 }

 public void accept(ICarElementVisitor visitor) {
 /*
 * 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);
 }
}

class Engine implements ICarElement {
 public void accept(ICarElementVisitor visitor) {
 visitor.visit(this);
 }
}

class Body implements ICarElement {
 public void accept(ICarElementVisitor visitor) {
 visitor.visit(this);
 }
}

class Car implements ICarElement {
 ICarElement[] elements;

 public Car() {
 //create new Array of elements
 this.elements = new ICarElement[] { new Wheel("front left"),
 new Wheel("front right"), new Wheel("back left"),
 new Wheel("back right"), new Body(), new Engine() };
 }

 public void accept(ICarElementVisitor visitor) {
 for(ICarElement elem : elements) {
 elem.accept(visitor);
 }
 visitor.visit(this);
 }
}

class CarElementPrintVisitor implements ICarElementVisitor {
 public void visit(Wheel wheel) {
 System.out.println("Visiting " + wheel.getName() + " wheel");
 }

 public void visit(Engine engine) {
 System.out.println("Visiting engine");
 }

 public void visit(Body body) {
 System.out.println("Visiting body");
 }

 public void visit(Car car) {
 System.out.println("Visiting car");
 }
}

class CarElementDoVisitor implements ICarElementVisitor {
 public void visit(Wheel wheel) {
 System.out.println("Kicking my " + wheel.getName() + " wheel");
 }

 public void visit(Engine engine) {
 System.out.println("Starting my engine");
 }

 public void visit(Body body) {
 System.out.println("Moving my body");
 }

 public void visit(Car car) {
 System.out.println("Starting my car");
 }
}

public class VisitorDemo {
 public static void main(String[] args) {
 ICarElement car = new Car();
 car.accept(new CarElementPrintVisitor());
 car.accept(new CarElementDoVisitor());
 }
}
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
(defclass auto
 ((elements :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 wheel (auto-part))

(defclass body (auto-part))

(defclass engine (auto-part))

(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))))

;; do-something visitations

;; catch all
(defmethod do-something (object other-object)
 (format t "don't know how ~s and ~s should interact~%" object other-object))

;; visitation involving wheel and integer
(defmethod do-something ((object wheel) (other-object integer))
 (format t "kicking wheel ~s ~s times~%" object other-object))

;; visitation involving wheel and symbol
(defmethod do-something ((object wheel) (other-object symbol))
 (format t "kicking wheel ~s symbolically using symbol ~s~%" object other-object))

(defmethod do-something ((object engine) (other-object integer))
 (format t "starting engine ~s ~s times~%" object other-object))

(defmethod do-something ((object engine) (other-object symbol))
 (format t "starting engine ~s symbolically using symbol ~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-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 #'print a *standard-output*)

 (terpri);; print newline

 ;; traverse with arbitrary context from other object
 (traverse #'do-something a 42)

 ;; traverse with arbitrary context from other object
 (traverse #'do-something a '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 е излишен в този случай.
Причината е, че е възможно да използваме анонимна функция която извиква желания метод с лексикално хванат обект:

(defmethod traverse (function (a auto));; other-object removed
 (with-slots (elements) a
 (dolist (e elements)
 (funcall function e))));; from here too

;; ...

 ;; alternative way to print-traverse
 (traverse (lambda (o) (print o *standard-output*)) a)

 ;; alternative way to do-something with
 ;; elements of a and integer 42
 (traverse (lambda (o) (do-something o 42)) a)

Множественото изпращане се случва при извикването подадено от тялото на анонимната функция, и обхождането и е мапваща функция която разпределя приложението на функцията върху елементите на обект.

По този начин всички следи от посетителския модел изчезват, освен мапващата функция, в която няма следа че 2 обекта са били използвани.

Всички следи че има 2 обекта и изпращането на техните типове е в ламбда функция.

Като изключим потенциалното подобряване на разделението от опасения, посетителският модел има допълнително предимство пред това просто да извикаме полиморфичен метод:посетителският обект може да има състояние.

Това е изключително полезно в много случаи в които действието извършвано върху обект зависи от предишни действия.

Пример за това е принтиращата мплементация в програмен език (като компилатор или интерпретатор).

Такъв принтиращ обект (имплементиран като посетител, в този случай), ще посети възлите в структура от данни които представляват парсната и обработена програма. Компилаторът ще принтира текстово представяне на програмното дърво. За да направи представянето четимо от хора, компилаторът трябва правилно да представи програмните конструкции и изрази.

Сегашното ниво на вдлъбнатина може да бъде следено от посетителят, както неговото състояние, така и коректно приложената енкапсулация, докато в обикновен полиморфичен метод призоваването и нивото на вдлъбнатост биха били прекалено открити като параметри и повиквателя ще разчита на методната имплементация да използва параметъра правилно.

Сродни модели на дизайн

[редактиране | редактиране на кода]
  • Командващ модел: Подобно на посетителския модел той енкапсулира една или повече функции в обект, за да ги представи пред повиквателя. За разлика от посетителския модел, командващият модел не прилага принципа за обхождане на обектовата структура.
  • Итераторен модел: Този модел дефинира обхождащ принцип подобно на посетителския модел, без обаче да прави разлика между типовете в обхождания обект.