Метафора
Данный паттерн можно сравнить с прохождением обследования в больнице. Однако «посетителем» в терминах паттернов здесь будут сами врачи. Чтобы было понятнее: у нас есть больной, которого требуется обследовать и полечить, но так как за разные обследования отвечают разные врачи, то мы просто присылаем к больному врачей в качестве «посетителей». Правило взаимодействия для больного очень простое «пригласите врача (посетителя) чтобы он сделал свою работу», а врач («посетитель») приходит, обследует и делает всё необходимое. Таким образом, следуя простым правилам можно использовать врачей для разных больных по одним и тем же алгоритмам. Как уже было сказано, паттерном «посетитель» в данном случае является врач, который может одинаково обслуживать разные объекты (больных) если его позовут.
Назначение
Применяя паттерн посетитель, мы можем добавить операции над некоторыми объектами (иерархией объектов), не загрязняя их код, т.е. не расширяя классы.
В классическом представлении GoF, посетитель добавляет операции к некоторой иерархии классов, где в зависимости от типа операции могут отличаться. Однако посетитель может быть применён и не к связанным иерархией классам.
Преимущества:
- Упрощается добавление новых операций, объединяет родственные операции в классе «Посетитель».
Недостатки:
- Бизнес логика размазывается между классами иерархии и классами посетителей (нарушается принцип Information Expert из GRASP),
- Усложнение структуры кода,
- Если базовый класс посетителя реализует интерфейс с указанием конкретного типа элемента из иерархии, при добавлении нового элемента в иерархию будет затруднительным, т.к. потребует модификации посетителей.
Диаграмма
Некоторая иерархия объектов Element реализует интерфейс с методом Accept, который в свою очередь принимает на вход элемент с интерфейсом Visitor. Внутри метода Accept вызывается метод visit для конкретного типа элемента (VisitElementOne или VisitElementTwo). Передавая тот или иной Visitor, мы тем самым можем выполнять над объектами Element разные операции, не расширяя при этом их интерфейс. Если бы ABAP умел поддерживать перегрузку методов, внутри элементов можно было бы сделать один вызов метода visit (v.Visit(me)), а его реализацию под тот или иной тип объекта уже реализовывать в конкретном посетителе.
Пример
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
*&---------------------------------------------------------------------* *& Определение и описание классов *&---------------------------------------------------------------------* CLASS lcl_element DEFINITION DEFERRED. CLASS lcl_visitor DEFINITION ABSTRACT. PUBLIC SECTION. METHODS: visit ABSTRACT IMPORTING io_element TYPE REF TO lcl_element. ENDCLASS. CLASS lcl_element DEFINITION ABSTRACT. PUBLIC SECTION. METHODS: accept IMPORTING io_visitor TYPE REF TO lcl_visitor. ENDCLASS. CLASS lcl_element IMPLEMENTATION. METHOD accept. io_visitor->visit( me ). ENDMETHOD. ENDCLASS. CLASS lcl_element_one DEFINITION INHERITING FROM lcl_element. PUBLIC SECTION. METHODS: operationA. ENDCLASS. CLASS lcl_element_one IMPLEMENTATION. METHOD operationA. WRITE: / 'Operation Element One'. ENDMETHOD. ENDCLASS. CLASS lcl_element_two DEFINITION INHERITING FROM lcl_element. PUBLIC SECTION. METHODS: operationB. ENDCLASS. CLASS lcl_element_two IMPLEMENTATION. METHOD operationB. WRITE: / 'Operation Element Two'. ENDMETHOD. ENDCLASS. CLASS lcl_visitor_one DEFINITION INHERITING FROM lcl_visitor. PUBLIC SECTION. METHODS: visit REDEFINITION. ENDCLASS. CLASS lcl_visitor_one IMPLEMENTATION. METHOD visit. DATA: lo_element_one TYPE REF TO lcl_element_one. TRY. lo_element_one ?= io_element. lo_element_one->operationa( ). CATCH cx_root. ENDTRY. ENDMETHOD. ENDCLASS. CLASS lcl_visitor_two DEFINITION INHERITING FROM lcl_visitor. PUBLIC SECTION. METHODS: visit REDEFINITION. ENDCLASS. CLASS lcl_visitor_two IMPLEMENTATION. METHOD visit. DATA: lo_element_two TYPE REF TO lcl_element_two. TRY. lo_element_two ?= io_element. lo_element_two->operationb( ). CATCH cx_sy_move_cast_error. RETURN. ENDTRY. ENDMETHOD. ENDCLASS. CLASS lcl_structure DEFINITION INHERITING FROM lcl_element. PUBLIC SECTION. METHODS: add_element IMPORTING io_element TYPE REF TO lcl_element, accept REDEFINITION. PRIVATE SECTION. DATA: mt_elements TYPE STANDARD TABLE OF REF TO lcl_element. ENDCLASS. CLASS lcl_structure IMPLEMENTATION. METHOD add_element. APPEND io_element TO mt_elements. ENDMETHOD. METHOD accept. DATA: lo_element TYPE REF TO lcl_element. LOOP AT mt_elements INTO lo_element. io_visitor->visit( lo_element ). ENDLOOP. ENDMETHOD. ENDCLASS. *&---------------------------------------------------------------------* *& Работка с объектами шаблона *&---------------------------------------------------------------------* START-OF-SELECTION. DATA: lo_structure TYPE REF TO lcl_structure, lo_el_one TYPE REF TO lcl_element_one, lo_el_two TYPE REF TO lcl_element_two, lo_vis_one TYPE REF TO lcl_visitor_one, lo_vis_two TYPE REF TO lcl_visitor_two. CREATE OBJECT lo_structure. CREATE OBJECT lo_el_one. CREATE OBJECT lo_el_two. " Операция над lcl_element_one CREATE OBJECT lo_vis_one. " Операция над lcl_element_two CREATE OBJECT lo_vis_two. lo_structure->add_element( lo_el_one ). lo_structure->add_element( lo_el_two ). lo_structure->accept( lo_vis_one ). lo_structure->accept( lo_vis_two ). |
В нашем примере мы создали двух посетителей, которые выполняют определённые операции над объектами. Каждая отдельная операция свойственна только конкретному типу элемента, соответственно нагружать общую иерархию и выносить это поведение (OperationA, OperationB) на верхний уровень иерархии не имеет смысла.
Так же мы не завязываемся на конкретные типы в интерфейсе посетителя, а оперируем абстракцией, что не столь оптимально, но зато нам не потребуется перестраивать интерфейс посетителя при добавлении в иерархию элементов нового класса.
Еще один пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
*&---------------------------------------------------------------------* *& Определение и описание классов *&---------------------------------------------------------------------* CLASS lcl_element DEFINITION DEFERRED. CLASS lcl_visitor DEFINITION ABSTRACT. PUBLIC SECTION. DATA: mv_cost TYPE decfloat34. METHODS: visit ABSTRACT IMPORTING io_element TYPE REF TO lcl_element. ENDCLASS. CLASS lcl_element DEFINITION ABSTRACT. PUBLIC SECTION. DATA: mv_cost TYPE decfloat34. METHODS: constructor IMPORTING iv_cost TYPE decfloat34, accept IMPORTING io_visitor TYPE REF TO lcl_visitor. ENDCLASS. CLASS lcl_element IMPLEMENTATION. METHOD accept. io_visitor->visit( me ). ENDMETHOD. METHOD constructor. mv_cost = iv_cost. ENDMETHOD. ENDCLASS. CLASS lcl_element_one DEFINITION INHERITING FROM lcl_element. ENDCLASS. CLASS lcl_element_one IMPLEMENTATION. ENDCLASS. CLASS lcl_element_two DEFINITION INHERITING FROM lcl_element. ENDCLASS. CLASS lcl_element_two IMPLEMENTATION. ENDCLASS. CLASS lcl_visitor_one DEFINITION INHERITING FROM lcl_visitor. PUBLIC SECTION. METHODS: visit REDEFINITION. ENDCLASS. CLASS lcl_visitor_one IMPLEMENTATION. METHOD visit. mv_cost = mv_cost + io_element->mv_cost. ENDMETHOD. ENDCLASS. CLASS lcl_visitor_two DEFINITION INHERITING FROM lcl_visitor. PUBLIC SECTION. METHODS: visit REDEFINITION. ENDCLASS. CLASS lcl_visitor_two IMPLEMENTATION. METHOD visit. mv_cost = mv_cost + io_element->mv_cost * -1. ENDMETHOD. ENDCLASS. CLASS lcl_structure DEFINITION. PUBLIC SECTION. METHODS: add_element IMPORTING io_element TYPE REF TO lcl_element, calculate_cost IMPORTING io_visitor TYPE REF TO lcl_visitor. PRIVATE SECTION. DATA: mt_elements TYPE STANDARD TABLE OF REF TO lcl_element. ENDCLASS. CLASS lcl_structure IMPLEMENTATION. METHOD add_element. APPEND io_element TO mt_elements. ENDMETHOD. METHOD calculate_cost. DATA: lo_element TYPE REF TO lcl_element. LOOP AT mt_elements INTO lo_element. io_visitor->visit( lo_element ). ENDLOOP. ENDMETHOD. ENDCLASS. *&---------------------------------------------------------------------* *& Работка с объектами шаблона *&---------------------------------------------------------------------* START-OF-SELECTION. DATA: lo_structure TYPE REF TO lcl_structure, lo_el_one TYPE REF TO lcl_element_one, lo_el_two TYPE REF TO lcl_element_two, lo_vis_one TYPE REF TO lcl_visitor_one, lo_vis_two TYPE REF TO lcl_visitor_two. CREATE OBJECT lo_structure. CREATE OBJECT lo_el_one EXPORTING iv_cost = 100. lo_structure->add_element( lo_el_one ). CREATE OBJECT lo_el_two EXPORTING iv_cost = 200. lo_structure->add_element( lo_el_two ). " Подсчёт прямой стоимости CREATE OBJECT lo_vis_one. " Подсчёт обратной стоимости CREATE OBJECT lo_vis_two. lo_structure->calculate_cost( lo_vis_one ). lo_structure->calculate_cost( lo_vis_two ). WRITE: / lo_vis_one->mv_cost. WRITE: / lo_vis_two->mv_cost. |
В данном примере у иерархии элементов единый интерфейс, а посетители используются для подсчёта некоторой абстрактной стоимости элемента, причём алгоритмы подсчёта у них разные. Алгоритм не завязан на тип объекта. Если потребуется высчитать некоторую другую стоимость по иному алгоритму, мы не будем нагромождать интерфейс иерархии, а создадим посетителя, который и выполнит нужный нам подсчёт (операцию над элементом).
Еще одна хорошая статья по теме.