Дизайн-шаблон Visitor из портфолио GoF (Часть 2)

На днях, коллеги обвинили меня в том, что я забыл дописать вторую часть статьи про Visitor. Я проверил, и действительно, это так 🙁 . Поэтому, сегодня мы продолжим знакомиться с этим дизайн-шаблоном. Первую часть статьи вы сможете найти тут: Дизайн-шаблон Visitor из портфолио GoF.

Давайте начнем “улучшать” предыдущий дизайн с того, что постараемся сделать так, чтобы наша структура файловой системы (ФС) не нарушала “Open-Close Principle”. Для этого нам необходимо обеспечить следующее:

  • Операции объявленные в интерфейсе Node, должны быть “универсальными”;
  • Кол-во этих операций должно быть сведено к минимуму;

Дизайн ФС, удовлетворяющий "Open/closed principle"

Соответственно, мы объявим всего одну-единственную операцию +accept(visitor:Visit). Не пугайтесь, дальше станет понятнее.

На что похожа эта операция?! Она похожа на входную дверь вашего дома, через которую к вам в гости может зайти как “Сантехник”, так и “Электрик”. “Друзья” и “Враги” тоже смогут проходить в вашу дверь, но только в том случае, если они будут реализовывать интерфейс Visitor, по имени которого и был назван сам шаблон. Я не зря перечислил друзей и врагов – смысл в том, что кол-во типов “посетителей” которые буду посещать вас и вашу библиотеку может разрастаться со временем без больших проблем для вашего проекта, но при одном “НО!”. Об этом “НО”, мы поговорим в конце статьи, поэтому, наберитесь терпения.

Все вышесказанное возможно только благодаря тому, что с точки зрения полиморфизма все “посетители” для классов вашей библиотеки буду равнозначны и соответствовать типу Visitor. Сам же интерфейс Node, теперь навязывает всем своим наследникам только один метод, который будет достаточно несложно реализовать в каждом из классов.

Теперь давайте разберемся с посетителями:

Visitor

Интерфейс Visitor определяет три перегруженных метода +visit(type:Type) для каждого типа данных с которым он сможет иметь дело. Тут вам может показаться, что мы противоречим сами себе и начинаем нарушать OCP, но уже в другой части дизайна 😉 . Отчасти вы будете правы, это и есть как раз то “НО!”, которое нам еще предстоит изучить. Но на самом деле разгадка прячется в том факте, что на самом деле контракт Visitor не будет расширяться, т.к. вряд ли вы сможете придумать в нашей файловой системе нового наследника Node. Понимаете?! Суть в том, что кол-во классов наследников Node у нас не будет расти, а значит и количество перегруженных методов visit(…) в будущем не увеличиться. Это, кстати, позволит нам избавиться от применения instanceof в нашем коде.

В тоже самое время, этот подход позволяет каждому конкретному посетителю работать с конкретным экземпляром класса наследника Node по своему, правильно реализуя конкретный метод visit(…). Под правильной обработкой мы можем понимать, как реализацию бизнес-логики, таки отсутствие таковой или генерирование исключительной ситуации.

Диаграммы готовы, теперь осталось понять как они взаимодействуют, разобраться во всей “магии” происходящего в классах.

В первом приближении использование шаблона выглядит следующим образом: Клиент, при возникновении конкретной потребности, создает (лично, либо через делегата) экземпляр конкретной команды, например “Сантехника” (обязательно реализующей интерфейс Visitor) и отправляет его в метод accept(Visitor visitor) конкретного экземпляра структуры, над которой нужно провести определенную операцию. Конкретный элемент нашей файловой системы, получив экземпляр посетителя (он не знает о его конкретном типе и работает с ним исключительно как с посетителем), вызывает у него метод visit(this) и передает самого себя в качестве аргумента. Очень часто на этом метод visit и заканчивается, но не обязательно 😉 .

Внимательно перечитайте последний абзац – именно в нем и объясняется работа шаблона.

Далее, если помните, в посетителе методы visit(…) перегружены под каждый тип данных и соответственно либо компилятор, либо виртуальная машина (VM) обеспечат подстановку нужного виртуального метода во время выполнения кода. Т.е. по сути мы избежали использование instanceof, переложив его вызовы на компилятор и VM. Сами же методы могут быть реализованы по разному – что позволяет, например, MkDirVisitor-у при посещении директории создать в ней новую поддиректорию, а при посещении файла – выбросить исключение, и т.д. и т.п.

В данном случае, мы получаем библиотеку посетителей, которая не нарушает ни OCP, ни LSP. А это значит, что мы можем свободно наращивать библиотеку наших команд в файловой системе, не перерабатывая саму ФС, просто реализовывая в каждой из команд по три метода для каждого элемента (Dir, File, List).

Теперь давайте поговорим про “НО”:

  • Все вышесказанное, справедливо только при условии, что структура данных (в нашем случае ФС) не будет разрастаться, т.к. появление нового наследника Node чревато для нас полной переработкой всей библиотеки команд 🙁
  • Внедрение шаблона Visitor оправдано в том случае, когда кол-во команд заранее не известно и будет расширяться / сужаться в будущем;
  • Шаблон плохо реализуется в платформах не поддерживающих перегрузку методов (например, Flex).

Ну, и напоследок – задание для пытливых и внимательных умов: Наверняка в ходе изложения вы захотели реализацию метода accept(Visitor visitor){visitor.visit(this);} вынести в абстрактный класс!? Что бы избежать copy/paste 😉 . Возможно ли это и если да, то в каком случае?

P.S. Ответы на вопрос вы можете разместить в комментариях к статье. Удачи!

This entry was posted in Java and tagged , , , . Bookmark the permalink.
  • Andrew S

    Здравствуйте, в чем смысл тогда node.accept(Vistior) если клиент сразу может передать node в visitor.visit(node) ?

    • Смысл в открытой и расширяемой библиотеки визиторов. Т.о. нода не знает заранее какая из операций будет произведена над ней – это случится в рантайме.

      • Andrew S

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

        • Смотрите, при использовании этого шаблона, вы можете практически до бесконечности наращивать библиотеку визиторов (команд), которые будут выполняться над нодами, при условии, что сама структура нод увеличиваться не будет. Визиторов может быть 10 / 100 / 1000, да сколько угодно, если это будет нужно.

          Если же начинать в коде анализировать саму ноду, то вы получите или if-else с 10 / 100 / 1000 вложений или столь же ужасный switch.

          На самом деле это иллюзия, т.к. эти проверки будут все равно где-то спрятаны (в случае с Java в механизм позднего связывания), но это очень красивая иллюзия.