Посетитель и Double Dispatch
Рассмотрим пример, в котором у нас есть небольшая иерархия классов геометрических фигур (осторожно, псевдокод):
Нам нужно добавить внешнюю операцию над всеми этими компонентами, например, экспорт. В нашем языке (Java, C#, ...) есть перегрузка методов, поэтому мы создаём такой класс:
Кажется, что всё хорошо. Но давайте испробуем такой класс в деле:
Как? Но почему?!
Побывать в шкуре компилятора
Примечание: всё что здесь описано — правда для большинства современных объектных языков программирования (Java, C#, PHP и другие).
Позднее/динамическое связывание
Давайте представим себя компилятором. Вам нужно понять как скомпилировать такой код:
Итак, вызов метода draw
в классе Shape
. Но нам известно ещё и о четырёх классах переопределяющих этот метод. Можно ли уже сейчас понять какую реализацию нужно выбрать? Похоже, что нет, ведь для этого придётся запустить программу и узнать какой же объект будет подан в параметр. Но одно вы знаете точно — какой бы объект ни был передан, он точно будет иметь реализацию draw
.
В результате машинный код, который вы создадите, будет каждый раз при проходе через этот участок проверять что за объект этот shape
, и выбирать реализацию метода draw
из соответствующего класса.
Такая динамическая проверка типа называется поздним или динамическим связыванием:
- Поздним, потому что мы связываем объект и реализацию уже после компиляции.
- Динамическим, потому что мы делаем это при каждом прохождении через этот участок.
Раннее/статическое связывание
Теперь давайте «скомпилируем» такой код:
С созданием объекта всё ясно. Как насчёт вызова метода export
? В классе Exporter
у нас есть пять версий метода с таким именем, которые отличаются только типом параметра. Похоже, здесь тоже придётся динамически отслеживать тип передаваемого параметра и по нему определять какой из методов выбрать.
Но здесь нас ждёт засада. Что если кто-то подаст в метод exportShape
такой объект, для которого не существует метода export
в классе Exporter
? Например, объект Ellipse
, для которого у нас нет экспорта. Действительно, у нас нет гарантии что необходимый метод будет существовать, как это было с переопределенными методами. А значит, возникнет неоднозначная ситуация.
Именно поэтому все разработчики компиляторов выбирают безопасную тропинку и применяют раннее или статическое связывание для перегруженных методов:
- Раннее, потому что оно происходит ещё на этапе компиляции программы.
- Статическое, потому что его уже не изменить во время выполнения.
Вернемся к нашему примеру. Мы уверены в том, что имеем параметр с типом Shape
. Мы знаем что в Exporter
существует подходящая реализация: export(s: Shape)
. Значит, этот участок кода мы жёстко связываем с известной реализацией метода.
И поэтому даже если мы подадим в параметрах один из подклассов Shape
, всё равно будет вызвана реализация export(s: Shape)
.
Double dispatch
Двойная диспетчеризация или double dispatch — это трюк, позволяющий обойти ограниченность раннего связывания в перегруженных методах. Вот как это делается:
Послесловие
Хотя паттерн Посетитель и построен на механизме двойной диспетчеризации, это не основная его идея. Посетитель позволяет добавлять операции к целой иерархии классов, без надобности менять код этих классов.