JS: Программирование, управляемое данными
2026-02-26 17:18 Diff

Решаемая задача: реализовать диспетчеризацию по типу своими руками.

Разложим весь процесс на примере библиотеки для работы с геометрическими фигурами. Предположим, что мы можем создавать разные фигуры, такие как треугольник, круг или квадрат. Кроме специфических свойств, у фигур есть и общие, например, периметр или площадь. А так как мы, гипотетически, хотим работать с фигурами единообразно, то реализуем диспетчеризацию по типу на примере функции, вычисляющей общую площадь фигур, размещенных на воображаемом холсте (так обычно называется область, на которой происходит рисование в графических редакторах)

При отсутствии готовой диспетчеризации нам придется делать ее руками в том месте, где требуется обобщенное поведение:

С наличием автоматического механизма диспетчеризации (не важно реализован он в самом языке или нами самостоятельно) код сокращается до следующего:

В примере выше функция getArea сама по себе не занимается вычислением площади. Это вычисление реализовано для каждой фигуры совершенно независимо. Все, что делает getArea, это перенаправляет запрос на расчет площади в соответствующую функцию.

Алгоритм диспетчеризации в примере выше следующий:

  1. getArea извлекает тип (его название) из фигуры.
  2. getArea обращается к глобальному хранилищу (виртуальная таблица) для поиска нужной реализации настоящей функции вычисления площади.
  3. Если реализация найдена, то getArea ее вызывает с нужными аргументами и возвращает результат наружу.

Важное следствие этого алгоритма в том, что для работы автоматической диспетчеризации необходимо, чтобы реальные функции getArea были занесены в виртуальную таблицу, иначе до них невозможно будет достучаться.

Виртуальная таблица

Выполняет две задачи, которые мы рассмотрим ниже.

Регистрация

Первая задача — это регистрация функций тех типов, по которым мы планируем делать диспетчеризацию:

Тогда модуль, реализующий наш тип, будет выглядеть так:

Как видно из примера выше, по большей части Circle является типичной абстракцией, за исключением пары моментов:

  1. Внутри создается привязка к типу. Соответственно все селекторы должны сначала извлечь данные и потом уже работать.
  2. С помощью definer происходит регистрация нужных (радиус специфичен для круга, по нему диспетчеризация не нужна) функций в нашей виртуальной таблице.

Наш модуль generic ничего не знает про Circle, да и вообще ничего не знает про тех, кто его использует. В общем случае, для регистрации функции ему нужно знать три значения: имя типа, имя функции и само тело функции, или, другими словами, мы имеем такой интерфейс: register('TypeName', 'funcName', funcBody). А код регистрации выглядел бы так:

Обратите внимание на то, что мы находимся внутри модуля Circle и нам приходится в каждом вызове register передавать его название. Это единственная причина, по которой существует функция defmethod. То есть мы сначала специфицируем имя типа для которого будем заполнять функции, а потом делаем это без повторений.

С точки зрения теории мы использовали так называемое частичное применение функции:

Что эквивалентно:

Ну и самое главное, а где же происходит регистрация? Куда записываются все эти данные о типах? Ответ достаточно простой. Фактически в наш прекрасный чистый код мы вводим внешнее изменяемое состояние и заполняем его функцией с побочными эффектами (definer). Если открыть модуль generic, то можно увидеть:

В свою очередь, все функции, которым нужен доступ к таблице, получают его посредством замыкания. Причем только definer изменяет ее, а все остальные - читают.

Получается, что methods наполняется в тот момент, когда загружаются типы (выполняется import), использующие модуль generic для регистрации своих функций. Например:

Поиск

Вторая задача это, собственно, поиск этих функций:

Для поиска подходящей функции достаточно знать два параметра: имя типа и имя функции. Если функция найдена, то getMethod возвращает ее вызывающему коду, который, в свою очередь, уже делает вызов найденной функции.