Посетитель и Double Dispatch
Рассмотрим пример, в котором у нас есть небольшая иерархия классов геометрических фигур (осторожно, псевдокод):
interface Graphic is
    method draw()
class Shape implements Graphic is
    field id
    method draw()
    // ...
class Dot extends Shape is
    field x, y
    method draw()
    // ...
class Circle extends Dot is
    field radius
    method draw()
    // ...
class Rectangle extends Shape is
    field width, height
    method draw()
    // ...
class CompoundGraphic implements Graphic is
    field children: array of Graphic
    method draw()
    // ...
Нам нужно добавить внешнюю операцию над всеми этими компонентами, например, экспорт. В нашем языке (Java, C#, ...) есть перегрузка методов, поэтому мы создаём такой класс:
class Exporter is
    method export(s: Shape) is
        print("Exporting shape")
    method export(d: Dot)
        print("Exporting dot")
    method export(c: Circle)
        print("Exporting circle")
    method export(r: Rectangle)
        print("Exporting rectangle")
    method export(cs: CompoundGraphic)
        print("Exporting compound")
Кажется, что всё хорошо. Но давайте испробуем такой класс в деле:
class App() is
    method export(shape: Shape) is
        Exporter exporter = new Exporter()
        exporter.export(shape);
app.export(new Circle());
// К сожалению, выведет "Exporting shape".
Как? Но почему?!
Побывать в шкуре компилятора
Примечание: всё что здесь описано — правда для большинства современных объектных языков программирования (Java, C#, PHP и другие).
Позднее/динамическое связывание
Давайте представим себя компилятором. Вам нужно понять как скомпилировать такой код:
method drawShape(shape: Shape) is
    shape.draw();
Итак, вызов метода draw в классе Shape. Но нам известно ещё и о четырёх классах переопределяющих этот метод. Можно ли уже сейчас понять какую реализацию нужно выбрать? Похоже, что нет, ведь для этого придётся запустить программу и узнать какой же объект будет подан в параметр. Но одно вы знаете точно — какой бы объект ни был передан, он точно будет иметь реализацию draw.
В результате машинный код, который вы создадите, будет каждый раз при проходе через этот участок проверять что за объект этот shape, и выбирать реализацию метода draw из соответствующего класса.
Такая динамическая проверка типа называется поздним или динамическим связыванием:
- Поздним, потому что мы связываем объект и реализацию уже после компиляции.
- Динамическим, потому что мы делаем это при каждом прохождении через этот участок.
Раннее/статическое связывание
Теперь давайте «скомпилируем» такой код:
method exportShape(shape: Shape) is
    Exporter exporter = new Exporter()
    exporter.export(shape);
С созданием объекта всё ясно. Как насчёт вызова метода export? В классе Exporter у нас есть пять версий метода с таким именем, которые отличаются только типом параметра. Похоже, здесь тоже придётся динамически отслеживать тип передаваемого параметра и по нему определять какой из методов выбрать.
Но здесь нас ждёт засада. Что если кто-то подаст в метод exportShape такой объект, для которого не существует метода export в классе Exporter? Например, объект Ellipse, для которого у нас нет экспорта. Действительно, у нас нет гарантии что необходимый метод будет существовать, как это было с переопределенными методами. А значит, возникнет неоднозначная ситуация.
Именно поэтому все разработчики компиляторов выбирают безопасную тропинку и применяют раннее или статическое связывание для перегруженных методов:
- Раннее, потому что оно происходит ещё на этапе компиляции программы.
- Статическое, потому что его уже не изменить во время выполнения.
Вернемся к нашему примеру. Мы уверены в том, что имеем параметр с типом Shape. Мы знаем что в Exporter существует подходящая реализация: export(s: Shape). Значит, этот участок кода мы жёстко связываем с известной реализацией метода.
И поэтому даже если мы подадим в параметрах один из подклассов Shape, всё равно будет вызвана реализация export(s: Shape).
Double dispatch
Двойная диспетчеризация или double dispatch — это трюк, позволяющий обойти ограниченность раннего связывания в перегруженных методах. Вот как это делается:
class Visitor is
    method visit(s: Shape) is
        print("Visited shape")
    method visit(d: Dot)
        print("Visited dot")
interface Graphic is
    method accept(v: Visitor)
class Shape implements Graphic is
    method accept(v: Visitor)
        // Компилятор знает, что здесь `this` это `Shape`.
        v.visit(this)
class Dot extends Shape is
    method accept(v: Visitor)
        // Компилятор знает, что здесь `this` это `Dot`.
        // А значит можно статически связать этот вызов
        // с реализацией visit(d: Dot).
        v.visit(this)
Visitor v = new Visitor();
Graphic g = new Dot();
// Метод accept() — переопределен, но не перегружен. А значит, связан
// динамически. Поэтому реализация `accept` будет выбрана во время выполнения
// уже из того класса, объект которого его вызвал (класс Dot).
g.accept(v);
// Выведет "Visited dot".
Послесловие
Хотя паттерн Посетитель и построен на механизме двойной диспетчеризации, это не основная его идея. Посетитель позволяет добавлять операции к целой иерархии классов, без надобности менять код этих классов.