Найти в Дзене
ZDG

Шаблоны проектирования: Визитёр

Оглавление

Предыдущие части: Фабрика, Синглтон, Стратегия, MVC, Вступление

Среди шаблонов проектирования есть такие, названия которых либо ни о чём не говорят, либо вводят в заблуждение (например, Стратегия).

Но есть и такие, которые работают именно так, как называются, и понятны просто интуитивно. Один из них –

Visitor

Давайте ещё раз вспомним, что предназначение шаблонов – упорядочить взаимодействие между различными объектами так, чтобы программа была надёжной и легко расширяемой.

Упорядочивание происходит в основном за счёт устранения зависимостей.

Что такое зависимость?

Это когда функционал какого-то объекта явным образом зависит от другого. Например, класс A явным образом упоминает класс B. Это когда исправление в каком-то месте программы приводит к множественным исправлениям в других местах. Это когда два программиста не могут одновременно работать над разным функционалом, потому что код пересекается.

Зависимости появляются, когда у объектов расширяется функционал. Чем сложнее устроен объект, тем больше вероятность, что в нём появятся зависимости.

Как борятся с зависимостями?

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

Какая задача решается?

Возьмём объект-человека, то есть вас. Ваше тело обладает рядом параметров – рост, вес, температура, размер одежды и т.д.

Вы также обладаете определённым набором методов-действий над своим телом. Например: постричь волосы, выпить чай, измерить температуру. И таких методов у вас много.

Если представить вас как программный объект, то он получится очень сложным, с огромным функционалом.

Теперь можно представить, что у вас есть собака. Но у неё тоже есть тело, со своими параметрами. И её тоже можно представить как объект со своим функционалом.

Очевидно, что вы и собака это объекты разного класса, но часть функционала у вас будет пересекаться (например: измерить температуру).

Значит, вам придётся организовывать некую схему наследования, либо же дублировать один и тот же функционал в разных классах.

Накидайте сюда ещё объектов, похожих по структуре и функционалу, но не совсем, и вы столкнетесь с проблемой нормализации всего этого зоопарка наследований и методов.

Игра Zoo Keeper
Игра Zoo Keeper

Ждём гостей!

Как говорилось выше, проблема зависимостей решается дроблением сложных объектов на более простые.

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

Например, парикмахер: умеет только стричь волосы. Доктор: умеет только мерять температуру. Если превратить их в код, то получим класс "парикмахер" с методом "стричь" и класс "доктор" с методом "мерять температуру".

Теперь вам не надо уметь выполнять эти функции. Вместо этого вы... приглашаете в гости доктора, если вам нужно померять температуру!

Очевидно, что того же самого доктора может пригласить и собака. Теперь измерение температуры – это отдельно существующее действие, которое может применяться ко многим объектам. Единственное требование для этого – у объекта должно быть свойство "температура", или он должен поддерживать интерфейс "ИзмеряемаяТемпература".

Посмотрите, как изменились зависимости:

  • Теперь метод измерения температуры только один, в своём отдельном классе, и его можно редактировать отдельно
  • Ни вам, ни собаке абсолютно не нужно знать, как меряется температура
  • Не нужно ничего наследовать, вы и собака теперь можете быть абсолютно неродственными объектами (хотя по факту это и не так :)
  • Температуру можно измерить даже у духовки – расширяемость кода повысилась!

Реализация

Объекты, поддерживающие данный шаблон, делятся на две категории: визитёры и хозяева. Хозяева должны иметь метод accept(), через который к ним приходит визитёр. Да, здесь можно навертеть интерфейс, но это сути не меняет.

В свою очередь, объект-визитёр должен иметь метод visit() для нанесения визита.

-2

Посмотрим на код, написанный на неизвестно каком языке. Я объявил класс Human, у которого есть свойство temperature.

Есть также класс VisitorMedic, который будет ходить в гости к Human. Он попадает туда через метод accept(), но дальше происходит интересное. Доктор попадает в метод как инертный объект, сам он ничего сделать не может. Поэтому Human буквально заставляет его навестить себя уже внутри своего accept(), дёргая у доктора метод visit():

v.visit(this)

this в данном контексте обозначает объект, в котором выполняется текущий метод. То есть сначала в объект human приходит доктор, а затем human передаёт самого себя (this) в метод visit() доктора, и там уже доктор меряет его температуру.

Таким же образом мы можем создать объекты-визитёры "парикмахер", "учитель" и т.д. и всех их направлять в human.accept(). Каждый из них будет делать свою работу, получив в своё распоряжение экземпляр human. И эти же объекты смогут навещать кого угодно другого.

Зачем accept()?

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

t = medic.visit(human)

?

Зачем нужно передавать доктора в accept()?

Действительно, в некоторых случаях мы спокойно можем вызвать напрямую medic.visit(human) и это вполне логично и нам за это ничего не будет.

Существование метода accept() обусловлено как минимум тремя обстоятельствами:

  1. Возможность выполнить несколько действий. Например, у human могут быть непубличные потомки, и в своём методе accept() он может устроить цикл и в цикле вызвать v.visit() для каждого потомка (соответственно там будет передаваться уже не this, а экземпляр потомка).
  2. Полиморфизм и компиляторы. Здесь я уже не могу сильно углубляться, просто дело в том, что метод visit() у визитёра может быть перегружен. Это значит, что в него могут передаваться разные классы, и в зависимости от каждого класса может быть реализована своя версия visit(). Но компилятор языка в некоторых случаях не знает заранее, что это будет и бла-бла-бла, в общем accept() нужен, чтобы передать this, а компилятор уже точно будет знать, что такое this.
  3. Вызывая v.visit() в своём методе accept(), класс может точно знать, когда манипуляции с ним начнутся и когда закончатся. Соответственно, он может сделать некую подготовку перед visit() и зачистку после visit().

Короче говоря, просто следуйте шаблону. Или не следуйте.

Пример

Например, здесь в редакторе можно выделить текст и назначить ему стиль: жирный, курсив, и т.д. Естественно, что со временем эти стили могут как расширяться, так и наоборот сокращаться.

Мы могли бы сделать класс TextSelection с методом accept(). Класс содержит выделенный текст и больше ничего. Далее, мы могли бы сделать BoldVisitor и отправить его в textSelection.accept(), чтобы он сделал текст жирным. Далее сделаем ItalicVisitor и тоже отправим в accept(), чтобы сделать текст курсивом и т.д. В случае, если появляется новый стиль, мы просто делаем нового визитёра. А если он становится ненужным, то просто удаляем класс визитёра.

Ценность данного решения в данном конкретном случае довольно спорна, так как мы не знаем всей архитектуры проекта.

Допустим, проще было бы реализовать метод, скажем, addTag() в самом TextSelection, и просто делать textSelection.addTag('b') или textSelection.addTag('i'). Но подумайте о том, что тексты есть не только в TextSelection, и тэги надо добавлять куда-то еще, и тогда практическая ценность визитёров вырастет, так как их можно будет применять в других местах.

А не стратегия ли это?

Ранее мы рассматривали шаблон Стратегия. Это тоже поведенческий шаблон, и он выполняет очень похожую функцию: в объект передаётся другой объект-стратегия, который затем выполняет необходимые действия. Так как стратегии можно менять, то и выполняемые действия можно менять. То есть стратегия делает то же самое, что визитёр?

Можно сказать, что да.

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

Кроме того, стратегия не обязательно совершает что-то с самим объектом. Она может делать вообще что угодно.

Визитёр же, как следует из названия, приходит и уходит. Он не остаётся. И работает он со свойствами объекта, к которому приходит. Хотя, конечно, ничто не мешает заставить его тоже делать всё что угодно. Но тогда мы нарушаем шаблон.

А шаблоны потому и разные, что у каждого своё предназначение.

Читайте дальше: