---
title: "Посетитель и двойная диспетчеризация"
type: "design-pattern"
slug: "visitor-double-dispatch"
url: "http://localhost:3000/ru/design-patterns/visitor-double-dispatch.md"
---
# Посетитель и двойная диспетчеризация

Давайте рассмотрим следующую иерархию классов геометрических фигур (осторожно, псевдокод):

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()
    // ...

Код прекрасно работает, и приложение уже в продакшене. Но однажды вы решили сделать функцию экспорта. Код экспорта выглядел бы чужеродным, если поместить его в эти классы. Поэтому вместо добавления экспорта во все классы этой иерархии вы решили создать новый класс, внешний по отношению к иерархии, и поместить всю логику экспорта внутрь него. У класса будут методы для экспорта публичного состояния каждого объекта в строки XML:

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);

Со второй строкой всё ясно: у класса `Exporter` нет собственного конструктора, так что мы просто создаём объект. А что насчёт вызова `export`? У `Exporter` есть пять методов с одинаковым именем, различающихся типами параметров. Какой из них вызвать? Похоже, здесь нам тоже понадобится динамическое связывание.

Но есть и другая проблема. Что, если существует класс фигуры, у которого нет подходящего метода `export` в классе `Exporter`? Например, объект `Ellipse`. Компилятор не может гарантировать наличие подходящего перегруженного метода, в отличие от переопределённых методов. Возникает неоднозначная ситуация, которую компилятор не может допустить.

Поэтому разработчики компиляторов идут безопасным путём и используют для перегруженных методов раннее (или статическое) связывание:

* **Раннее**, потому что оно происходит во время компиляции, до запуска программы.
* **Статическое**, потому что его нельзя изменить во время выполнения.

Вернёмся к нашему примеру. Мы уверены, что входящий аргумент будет из иерархии `Shape`: либо класс `Shape`, либо один из его подклассов. Мы также знаем, что у класса `Exporter` есть базовая реализация экспорта, поддерживающая класс `Shape`: `export(s: Shape)`.

Это единственная реализация, которую можно безопасно связать с данным кодом, не создавая неоднозначности. Вот почему, даже если мы передадим объект `Rectangle` в `exportShape`, экспортёр всё равно вызовет метод `export(s: Shape)`.

## Двойная диспетчеризация

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

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`.
        // А значит, можно безопасно вызвать `visit(s: Shape)`.
        v.visit(this)

class Dot extends Shape is
    method accept(v: Visitor)
        // Компилятор знает, что `this` — это `Dot`.
        // А значит, можно безопасно вызвать `visit(s: Dot)`.
        v.visit(this)

Visitor v = new Visitor();
Graphic g = new Dot();

// Метод `accept` переопределён, а не перегружен. Компилятор связывает его
// динамически. Поэтому `accept` будет выполнен у того класса, который
// соответствует объекту, вызывающему метод (в нашем случае — класса `Dot`).
g.accept(v);

// Вывод: "Visited dot"

### Послесловие

Хотя паттерн [Посетитель](/ru/design-patterns/visitor) построен на принципе двойной диспетчеризации, это не его основное назначение. Посетитель позволяет добавлять «внешние» операции ко всей иерархии классов, не меняя существующий код этих классов.
