Видео курс Шаблоны проектирования

Основные темы, рассматриваемые на уроке:
00:33 1 Метафора
05:02 2 Программный код метафоры
18:03 3 Структура паттерна на языке UML
18:03 4 Структура паттерна на языке UML
20:03 5 Структура паттерна на языке C#
22:26 6 Альтернативное использование паттерна (C#)
27:56 7 Назначение паттерна
33:03 8 Завершение курса (Пожелания и рекомендации)

Видео урок посвящен шаблону проектирования Visitor (Посетитель), который позволяет единообразно обойти набор элементов с разнородными интерфейсами (т.е. набор объектов разных классов не приводя их к общему базовому типу), а также позволяет добавить новый метод (функцию) в класс объекта, при этом не изменяя сам класс этого объекта.

Для просмотра полной версии видеокурса и получения доступа к дополнительным учебным материалам Вам необходимо оформить подписку
Оформить подписку

Паттерн Visitor

Название

Посетитель

Также известен как

Walker (Бродяга)

Классификация

По цели: поведенческий

По применимости: к объектам

Частота использования

Низкая                  -   1 2 3 4 5

Назначение

Паттерн Visitor – позволяет единообразно обойти набор элементов с разнородными интерфейсами (т.е. набор объектов разных классов не приводя их к общему базовому типу), а также позволяет добавить новый метод (функцию) в класс объекта, при этом не изменяя сам класс этого объекта.

Введение

Предлагается начать рассмотрение паттерна Visitor с использованием метафоры. На минуту заставим себя поверить в существование такого сказочного персонажа как Дед Мороз, который в Новогоднюю ночь поочередно посещает домики в которых живут дети и дарит им подарки. На рисунке ниже, представлена деревня (объектная структура) в которой имеется только два домика (Элемент А и Элемент Б), в одном из них живет мальчик, а в другом девочка. 

 

Понятно, что деревню можно программно представить, как коллекцию домиков. В деревне могут появляться новые домики, а старые ветшать и удаляться из коллекции.

Задача Деда Мороза посетить каждый домик и исполнить желания каждого ребенка (кто что пожелает), другими словами выполнить определенные операции в определенном домике. Например, Дед Мороз мальчику расскажет «волшебную сказку» а девочке подарит «платье прекрасной голубой феи».

Из сказанного выше легко сформировать модель и реализовать ее программно.

См. пример к главе: \023_Visitor\002_NewYear

Хотелось бы обратить внимание на несколько технических особенностей, которые могли бы смутить читателя в процессе рассмотрения примера.

Возникает, справедливый на первый взгляд вопрос: Почему методы VisitBoysHouse и VisitGirlsHouse в классе Visitor используют в качестве параметра типы конкретных классов BoysHouse и GirlsHouse

Ведь если, мы будем использовать абстрактный класс Element в качестве типа параметра, то так будет правильней и красивее. Но тут возникает проблема, что после приведения к базовому типу Element, нам становятся недоступными для использования операции конкретных элементов (домиков).

Въедливые умы сразу могут предложить создать абстрактные методы TellFairyTale и CiveDress в базовом абстрактном классе Element, реализовать их в производных классах и код успешно выполниться. Технически организовать такую реализацию не составит труда и пример ниже демонстрирует такой подход.

Как видно из примера, мальчик и девочка расширили свой интерфейс взаимодействия с Дедом Морозом, и остается задуматься над реализацией абстрактных методов. Мальчик теперь имеет возможность получить в подарок платье прекрасной голубой феи, а девочка имеет возможность слушать сказки которые она у Деда Мороза не заказывала и вообще не любит сказок (но это еще терпимо). Известно, что добавлять не нужную функциональность тому или иному объекту, не является успешным подходом при построении объектно-ориентированных систем.

Более того, важно то, что оказался испорчен интерфейс взаимодействия с объектами типа Element, теперь интерфейс взаимодействия компрометирует абстракцию типа. Аналогично, если читатель являясь мужчиной, публично перед коллегами и друзьями выразит готовность принять в подарок на новый год платье прекрасной голубой феи (и обязательно готовность предстать в нем). Это и есть компрометация абстракции типа «настоящий мужчина». Компрометация абстракции влечет за собой нарушение концептуальной целостности системы и так далее по наклонной. Поэтому авторы паттерна Visitor отказались от использования подхода с обобщением поведений (методов) классов BoysHouse и GirlsHouse, и нельзя назвать существующий подход с использование конкретных классов красивым, удобным и способствующим повторному использованию, но такой подход можно назвать вынужденным.

Таким образом из примера видно, что посетитель (в нашем случае Дед Мороз) единообразно обошел элементы (домики) из объектной структуры (деревни), несмотря на наличие разнородности в интерфейсах элементов (домиков) BoysHouse и GirlsHouse, а это и является одной из важнейших задач которую помогает решить использование паттерна Visitor.

Структура паттерна на языке UML

См. пример к главе: \023_Visitor\001_Visitor

Структура паттерна на языке C#

См. пример к главе: \023_Visitor\001_Visitor

Участники

  • Visitor - Посетитель:

Предоставляет абстрактный интерфейс (набор методов VisitConcretElementX) для работы с объектами класса ConcreteElementX. Имя метода VisitConcretElementX включает в себя имя класса, экземпляр которого вызывает данный метод.

  • ConcreteVisitor - Конкретный посетитель:

Реализует абстрактный интерфейс предоставляемый абстрактным классом Visitor. Каждая операция VisitConcretElementX реализует фрагмент алгоритма, специфичного для каждого отдельного класса ConcreteElement.

  • Element - элемент:

Предоставляет абстрактный метод Accept, который принимает аргумент типа Visitor.

  • ConcreteElement - Конкретный элемент:

Реализует абстрактный метод Accept, который принимает аргумент типа Visitor.

  • ObjectStructure - Структура объектов:

Представляет собой набор объектов типа Element. Может быть, как обычной коллекцией, так и древообразной структурой.

Отношения между участниками

Отношения между классами

  • Конкретные классы ConcreteVisitor связаны связью отношения наследования с абстрактным классом Visitor.
  • Конкретные классы ConcreteElement связаны связью отношения наследования с абстрактным классом Element.
  • Конкретный класс ObjectStructure связан связью отношения ассоциации с абстрактным классом Element.

Отношения между объектами

  • Клиент использующий паттерн Visitor должен создать экземпляр класса ConcreteVisitor и с его помощью обойти каждый элемент объектной структуры (коллекции).
  • При посещении экземпляром класса ConcreteVisitor определенного элемента (экземпляра класса ConcreteElement) из объектной структуры, этот элемент вызывает на посетителе метод VisitConcretElementX, соответствующий классу данного элемента. Элемент передает этому методу себя в качестве аргумента, чтобы посетитель мог получить доступ к членам (состоянию и поведению) данного элемента.

На представленной ниже диаграмме взаимодействия показаны отношения между объектами: клиентом (Program), объектной структурой (ObjectStructure), посетителем (ConcreteVisitor) и двумя элементами (ConcreteElementA и ConcreteElementB).

См. пример к главе: \023_Visitor\001_Visitor

Мотивация

Предлагается рассмотреть работу простейшего компилятора, который представляет код исходной программы в виде синтаксического дерева разбора. Над деревьями разбора компилятор должен выполнять операции «статического семантического» анализа, например, проверять что все переменные определены. Также компилятор должен сгенерировать исполняемый код. Аналогично можно реализовать операции контроля типов, оптимизации исполняемого кода, анализа потока исполнения, проверки гарантированной инициализации переменной перед первым использованием, и т.д.

Применимость паттерна

Паттерн Visitor рекомендуется использовать, когда:

  • В (гетерогенной) коллекции (ObjectStructure) должны присутствовать разнотипные объекты (ConcreteElementA и ConcreteElementB) с разнородными интерфейсами и при этом требуется организовать унифицированный обход элементов этой коллекции и выполнить определенные операции над каждым имеющимся объектом.
  • Над объектами (ConcreteElementA и ConcreteElementB) входящими в состав коллекции (ObjectStructure) требуется выполнять определенные операции и при этом не хотелось бы повторять эти операции в каждом классе (ConcreteElementA и ConcreteElementB). Код этих операций можно вынести в методы объекта посетителя класса ConcreteVisitor.
  • Классы объектов (ConcreteElementA и ConcreteElementB), входящих в коллекцию (ObjectStructure) изменяются редко, но новые операции, производимые над коллекцией требуется добавлять часто. Важно понимать, что при изменении интерфейса классов объектов (ConcreteElementA и ConcreteElementB), входящих в состав коллекции, вероятней всего потребуется переопределить и интерфейсы всех объектов посетителей (ConcreteVisitor1 и ConcreteVisitor2), а это может быть затруднительно. Поэтому, если классы (ConcreteElementA и ConcreteElementB) изменяются часто, то лучше все операции определять прямо в них (т.е., не выносить операции в посетителей).

Пример отображающий идеи применимости паттерна Visitor, рассмотренные выше.

См. пример к главе: \023_Visitor\003_Visitor

Результаты

Паттерн Visitor обладает следующими преимуществами:

  • Упрощение добавления новых методов.

Используя объекты-посетители, легко добавлять новую функциональность объектам-элементам. Понятно, что функциональность, предназначенная для объектов-элементов, физически будет находиться в методах объекта-посетителя, создавая иллюзию расширения самого класса элементов новой функциональностью. Для добавления новой операции (функциональности) всем или некоторым объектам-элементам, потребуется создать новый класс посетителя или изменить один из существующих классов посетителей.

  • Объединение сходного поведения.

Сходное поведение (функциональность) относящееся к объектам-элементам, не разносится по всем классам (ConcreteElement), а локализуется (размещается) в классе объекта-посетителя. Не связанная друг с другом функциональность распределяется по отдельным конкретным классам (ConcreteVisitor). Такой подход позволяет упростить как сами классы элементов (ConcreteElement), так и алгоритм, располагающийся внутри посетителя (все данные которые относятся к алгоритму можно располагать непосредственно в посетителе, т.е. рядом с алгоритмом).

  • Накопление состояния.

Посетитель может накапливать в себе информацию о состоянии элементов, входящих в объектную структуру. Если не использовать паттерн Visitor, то состояние придется передавать в виде дополнительных (ref/out) параметров методов, выполняющих обход и сохранять в специально отведенном месте, что может вызвать определенные неудобства.

Паттерн Visitor обладает следующими недостатками:

  • Сложность добавления новых классов ConcreteElement.

При использовании паттерна Visitor, возникают некоторые сложности при добавлении новых классов элементов (ConcreteElement). Связано это с тем, что создание нового класса ConcreteElement, требует добавления нового абстрактного метода VisitConcretElementX в абстрактный класс Visitor, этот абстрактный метод потребуется реализовать во всех производных классах ConcreteVisitor.

При принятии решения об использования паттерна Visitor, потребуется определиться, что чаще будет добавляться в подсистему: алгоритм (новая функциональность) применяемый к элементам (ConcreteElement) входящим в объектную структуру или сами классы этих элементов (ConcreteElement). Если чаще будет добавляться алгоритм, то паттерн Visitor, поможет лучше управлять такими изменениями. Если же, планируется часто добавлять новые классы элементов (ConcreteElement), то вероятно, что такую модель классов сопровождать будет не удобно, но вполне возможно.

  • Разрушение инкапсуляции.

При работе с элементами (ConcreteElement), объект-посетитель в большинстве случаев должен знать их внутреннее устройство, для того чтобы справиться со своей работой по расширению функциональности. Поэтому при использовании паттерна Visitor, требуется организовывать более широкий чем мог бы быть, открытый интерфейс взаимодействия с объектами (ConcreteElement), что естественно, разрушает инкапсуляцию.

Реализация

С каждым объектом-элементом класса (ConcreteElement), ассоциирован некий класс (ConcreteVisitor) объекта-посетителя. В классах объектов посетителей (ConcreteVisitor), реализована операция VisitConcretElement, для каждого конкретного класса ConcreteElement. В каждой операции VisitConcretElement, имеется аргумент одного из классов ConcreteElement, благодаря этому посетитель может получить доступ к открытому (public) интерфейсу класса ConcreteElement. Классы ConcreteVisitor реализуют абстрактные методы VisitConcretElement из базового класса Visitor, с целью реализации в посетителе специфического поведения (функциональности), предназначенного для соответствующего класса ConcreteElement.

Каждый класс ConcreteElement реализует метод Accept, который вызывает соответствующий метод VisitConcretElement, на посетителе. Следовательно, какая будет в конечном итоге вызвана операция: OperationA или OperationB, зависит как от класса элемента ConcreteElement, так и от класса посетителя ConcreteVisitor.

Можно было бы использовать перегрузку методов, и вместо методов с разными оттенками имен VisitConcretElementX и VisitConcretElementY, воспользоваться методами с одним именем - Visit. Имеется два мнения по поводу использования перегрузки методов, одно за, другое против. Сторонники перегрузки подчеркивают, что все методы выполняют однотипную деятельность, хоть и имеют разные аргументы. Противники перегрузки бояться, что читателю программы будет затруднительно понимать, что именно происходит (кого требуется посетить) при вызове метода с именем Visit. Считаются оба варианта приемлемыми (допустимыми), применимость перегрузки зависит от предпочтений программиста.

При решении вопроса о применении паттерна Visitor часто возникают два спорных момента:

  • Техника двойной диспетчеризации.

Понятие «Двойная диспетчеризация» ("Множественная диспетчеризация", "Мультиметод") означает, технику подбора метода класса в зависимости от возвращаемого типа и типов значений аргументов. Язык С# имеет встроенную поддержку такой техники в виде методов расширения (Extention Methods). Методы расширения позволяют "добавлять" методы в существующие типы без создания нового производного типа, перекомпиляции или иного изменения исходного типа. Расширяющие методы могут быть только статическими и создаваться только в статических классах.

static class MyClass 
{ 
    public static void Method(this int value) 
    { 
        Console.WriteLine(value); 
    } 
}
  • Кто ответственный за обход объектной структуры.

Объект-посетитель должен посетить (обойти) каждый объект-элемент, находящийся в объектной структуре (коллекции). Возникает вопрос: каким образом посетитель должен получить доступ к элементам объектной структуры и совершить обход, то есть непосредственно обратиться к каждому элементу? Обход можно организовать тремя способами:

  1. Объектная структура (отвечает за обход) принимает ссылку на посетителя и в цикле передает ссылку на посетителя как аргумент метода Accept текущего элемента.
  2. Объектная структура предоставляет посетителю объект итератор и посетитель самостоятельно поочередно запрашивает у итератора объекты-элементы и работает с ними.
  3. Объектная структура «пускает посетителя в себя», другими словами предоставляет прямой доступ к массиву объектов-элементов и в этом случае сам посетитель начинает играть роль итератора.

Чаще всего сама объектная структура (коллекция) отвечает за обход элементов и такой подход является предпочтительным. Объектная структура обходит все элементы и вызывает на каждом метод Accept. Если объектная структура имеет древообразную организацию, то в таком случае, «дерево обходит само себя», рекурсивно вызывая метод Accept на каждом элементе и в качестве аргумента метода Accept предавая ссылку на посетителя. 

© 2017 ITVDN, все права защищены