Предыдущие части: Фабрика, Синглтон, Стратегия, MVC, Вступление
Среди шаблонов проектирования есть такие, названия которых либо ни о чём не говорят, либо вводят в заблуждение (например, Стратегия).
Но есть и такие, которые работают именно так, как называются, и понятны просто интуитивно. Один из них –
Visitor
Давайте ещё раз вспомним, что предназначение шаблонов – упорядочить взаимодействие между различными объектами так, чтобы программа была надёжной и легко расширяемой.
Упорядочивание происходит в основном за счёт устранения зависимостей.
Что такое зависимость?
Это когда функционал какого-то объекта явным образом зависит от другого. Например, класс A явным образом упоминает класс B. Это когда исправление в каком-то месте программы приводит к множественным исправлениям в других местах. Это когда два программиста не могут одновременно работать над разным функционалом, потому что код пересекается.
Зависимости появляются, когда у объектов расширяется функционал. Чем сложнее устроен объект, тем больше вероятность, что в нём появятся зависимости.
Как борятся с зависимостями?
В подавляющем большинстве случаев сложные объекты дробятся на простые, а жёсткие связи заменяются цепочкой посредников и абстракций. Всё это плодит лишние классы, но также позволяет относительно безболезненно расширять программу и коллективно работать над кодом.
Какая задача решается?
Возьмём объект-человека, то есть вас. Ваше тело обладает рядом параметров – рост, вес, температура, размер одежды и т.д.
Вы также обладаете определённым набором методов-действий над своим телом. Например: постричь волосы, выпить чай, измерить температуру. И таких методов у вас много.
Если представить вас как программный объект, то он получится очень сложным, с огромным функционалом.
Теперь можно представить, что у вас есть собака. Но у неё тоже есть тело, со своими параметрами. И её тоже можно представить как объект со своим функционалом.
Очевидно, что вы и собака это объекты разного класса, но часть функционала у вас будет пересекаться (например: измерить температуру).
Значит, вам придётся организовывать некую схему наследования, либо же дублировать один и тот же функционал в разных классах.
Накидайте сюда ещё объектов, похожих по структуре и функционалу, но не совсем, и вы столкнетесь с проблемой нормализации всего этого зоопарка наследований и методов.
Ждём гостей!
Как говорилось выше, проблема зависимостей решается дроблением сложных объектов на более простые.
Представьте, что каждую функцию, которую можете выполнять вы, может выполнять кто-то другой, но он может выполнять только эту функцию и ничего больше.
Например, парикмахер: умеет только стричь волосы. Доктор: умеет только мерять температуру. Если превратить их в код, то получим класс "парикмахер" с методом "стричь" и класс "доктор" с методом "мерять температуру".
Теперь вам не надо уметь выполнять эти функции. Вместо этого вы... приглашаете в гости доктора, если вам нужно померять температуру!
Очевидно, что того же самого доктора может пригласить и собака. Теперь измерение температуры – это отдельно существующее действие, которое может применяться ко многим объектам. Единственное требование для этого – у объекта должно быть свойство "температура", или он должен поддерживать интерфейс "ИзмеряемаяТемпература".
Посмотрите, как изменились зависимости:
- Теперь метод измерения температуры только один, в своём отдельном классе, и его можно редактировать отдельно
- Ни вам, ни собаке абсолютно не нужно знать, как меряется температура
- Не нужно ничего наследовать, вы и собака теперь можете быть абсолютно неродственными объектами (хотя по факту это и не так :)
- Температуру можно измерить даже у духовки – расширяемость кода повысилась!
Реализация
Объекты, поддерживающие данный шаблон, делятся на две категории: визитёры и хозяева. Хозяева должны иметь метод accept(), через который к ним приходит визитёр. Да, здесь можно навертеть интерфейс, но это сути не меняет.
В свою очередь, объект-визитёр должен иметь метод visit() для нанесения визита.
Посмотрим на код, написанный на неизвестно каком языке. Я объявил класс 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() обусловлено как минимум тремя обстоятельствами:
- Возможность выполнить несколько действий. Например, у human могут быть непубличные потомки, и в своём методе accept() он может устроить цикл и в цикле вызвать v.visit() для каждого потомка (соответственно там будет передаваться уже не this, а экземпляр потомка).
- Полиморфизм и компиляторы. Здесь я уже не могу сильно углубляться, просто дело в том, что метод visit() у визитёра может быть перегружен. Это значит, что в него могут передаваться разные классы, и в зависимости от каждого класса может быть реализована своя версия visit(). Но компилятор языка в некоторых случаях не знает заранее, что это будет и бла-бла-бла, в общем accept() нужен, чтобы передать this, а компилятор уже точно будет знать, что такое this.
- Вызывая v.visit() в своём методе accept(), класс может точно знать, когда манипуляции с ним начнутся и когда закончатся. Соответственно, он может сделать некую подготовку перед visit() и зачистку после visit().
Короче говоря, просто следуйте шаблону. Или не следуйте.
Пример
Например, здесь в редакторе можно выделить текст и назначить ему стиль: жирный, курсив, и т.д. Естественно, что со временем эти стили могут как расширяться, так и наоборот сокращаться.
Мы могли бы сделать класс TextSelection с методом accept(). Класс содержит выделенный текст и больше ничего. Далее, мы могли бы сделать BoldVisitor и отправить его в textSelection.accept(), чтобы он сделал текст жирным. Далее сделаем ItalicVisitor и тоже отправим в accept(), чтобы сделать текст курсивом и т.д. В случае, если появляется новый стиль, мы просто делаем нового визитёра. А если он становится ненужным, то просто удаляем класс визитёра.
Ценность данного решения в данном конкретном случае довольно спорна, так как мы не знаем всей архитектуры проекта.
Допустим, проще было бы реализовать метод, скажем, addTag() в самом TextSelection, и просто делать textSelection.addTag('b') или textSelection.addTag('i'). Но подумайте о том, что тексты есть не только в TextSelection, и тэги надо добавлять куда-то еще, и тогда практическая ценность визитёров вырастет, так как их можно будет применять в других местах.
А не стратегия ли это?
Ранее мы рассматривали шаблон Стратегия. Это тоже поведенческий шаблон, и он выполняет очень похожую функцию: в объект передаётся другой объект-стратегия, который затем выполняет необходимые действия. Так как стратегии можно менять, то и выполняемые действия можно менять. То есть стратегия делает то же самое, что визитёр?
Можно сказать, что да.
Отличие, однако, в том, что стратегия это долговременное решение. Объект-стратегия сохраняется внутри объекта-владельца (то есть необходимо дополнительное поле для хранения) и действует постоянно, пока не заменён другой стратегией.
Кроме того, стратегия не обязательно совершает что-то с самим объектом. Она может делать вообще что угодно.
Визитёр же, как следует из названия, приходит и уходит. Он не остаётся. И работает он со свойствами объекта, к которому приходит. Хотя, конечно, ничто не мешает заставить его тоже делать всё что угодно. Но тогда мы нарушаем шаблон.
А шаблоны потому и разные, что у каждого своё предназначение.
Читайте дальше: