Services и dependency injection

В архитектуре Mapbox GL JS система «сервисов» представляет собой совокупность внутренних подсистем, каждая из которых отвечает за строго определённый участок работы: загрузку данных, управление стилем, отрисовку, обработку событий, работу с источниками данных и взаимодействие с WebGL-рендерером. Эти компоненты не существуют изолированно — они связаны через принципы инверсии управления и внедрения зависимостей, где центральный объект карты выступает контейнером контекста и поставщиком зависимостей.

В основе Mapbox GL JS лежит модульная архитектура, где функциональность разбита на сервисные домены:

  • управление картой и состоянием (Map, Transform)
  • работа со стилем (Style, StyleLayer, StyleImage)
  • источники данных (SourceCache, GeoJSONSource, VectorTileSource)
  • рендеринг (Painter, Program, Shader)
  • загрузка ресурсов (RequestManager, ResourceLoader)
  • выполнение фоновых задач (WorkerPool, TileWorker)
  • обработка событий (Evented)

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

Ключевая идея заключается в том, что ни один компонент не создаётся в полной изоляции — он получает необходимые зависимости от окружения карты.

Карта как контейнер зависимостей

Объект карты (Map) играет роль центрального контейнера, который агрегирует и распределяет зависимости. При инициализации он создаёт и связывает все ключевые сервисы:

  • трансформацию координат (Transform)
  • менеджер стиля (Style)
  • очередь запросов (RequestManager)
  • менеджер тайлов (TileManager)
  • WebGL контекст и рендерер (Painter)

Каждый сервис получает доступ к общему состоянию через ссылку на карту или через переданные при создании параметры.

Такой подход можно рассматривать как упрощённую форму dependency injection container, где Map выступает в роли root provider.

Сервис стиля и управление визуальным состоянием

Сервис Style отвечает за интерпретацию Mapbox Style Specification и преобразование декларативного описания карты в набор исполняемых структур.

Он управляет:

  • слоями (layers)
  • источниками данных (sources)
  • спрайтами (sprite)
  • шрифтами (glyphs)
  • изображениями (images)

Style не выполняет рендеринг напрямую, но формирует промежуточное представление, которое затем потребляется рендерером.

Внутри архитектуры он зависит от:

  • загрузчика ресурсов (для sprite/glyph tiles)
  • системы событий (для уведомления об изменениях)
  • тайловых источников (для получения данных)

Таким образом, Style является сервисом-оркестратором, координирующим работу других подсистем.

Сервис источников данных (SourceCache)

SourceCache управляет жизненным циклом источников данных и их тайлов. Он инкапсулирует:

  • кэширование тайлов
  • запросы к сетевым ресурсам
  • обновление данных при изменении viewport
  • синхронизацию с worker-потоками

Каждый источник (VectorTileSource, RasterSource, GeoJSONSource) внедряется в систему через SourceCache, который выступает посредником между стилем и рендерером.

Зависимости источника включают:

  • менеджер запросов
  • систему тайлов
  • worker-пул

Таким образом реализуется слабая связность между источниками и рендеринговым ядром.

Worker-сервисы и изоляция вычислений

Одной из ключевых особенностей Mapbox GL JS является использование Web Workers для тяжёлых вычислений:

  • генерация геометрии
  • обработка векторных тайлов
  • подготовка данных для рендеринга

Worker-сервис (WorkerPool, TileWorker) получает «внедрённый» контекст в виде сериализованных сообщений. Это форма dependency injection через message passing.

Вместо прямых зависимостей используется контракт:

  • входные данные (tile, style, zoom)
  • набор инструкций (parse, transform, simplify)
  • выходные данные (vertex buffers, feature sets)

Таким образом, зависимости «передаются» не через ссылки, а через структурированные сообщения.

Рендеринг как слой потребления сервисов

Сервис Painter отвечает за финальный этап — отрисовку WebGL-сцен.

Он зависит от:

  • Style (что рисовать)
  • Transform (где рисовать)
  • SourceCache (какие данные доступны)
  • TextureManager (текстуры и изображения)

Painter не знает ничего о загрузке данных или сетевых запросах — он работает только с уже подготовленным состоянием.

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

Система событий как внедряемая зависимость

Evented — базовый механизм событий в Mapbox GL JS — также является сервисом, внедряемым во многие компоненты.

Он обеспечивает:

  • подписку на изменения состояния карты
  • уведомления о загрузке ресурсов
  • синхронизацию между слоями и источниками

Практически каждый сервис зависит от event bus, но не реализует его самостоятельно. Это предотвращает циклические зависимости и позволяет централизовать управление состоянием.

Контекстные зависимости и скрытое DI

Хотя Mapbox GL JS не использует классический dependency injection контейнер (как в Angular или Spring), он реализует аналог через:

  • передачу map как глобального контекста
  • конструкторы сервисов с набором зависимостей
  • внутренние фабрики (createStyle, createPainter, createSource)

Пример паттерна:

  • сервис создаётся внутри Map
  • получает ссылку на map
  • извлекает необходимые зависимости через неё

Это создаёт гибридную модель: частично явное DI, частично service locator.

Иерархия зависимостей внутри системы

Сервисы образуют направленный граф зависимостей:

  • Map (root container)

    • Style

      • SourceCache
      • ImageManager
    • Painter

      • Style
      • Transform
    • SourceCache

      • WorkerPool
    • WorkerPool

      • RequestManager

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

Динамическая инъекция и обновление сервисов

Mapbox GL JS поддерживает изменение состояния карты во время выполнения:

  • смена стиля (setStyle)
  • добавление источников (addSource)
  • обновление слоёв (addLayer, removeLayer)

Каждое такое изменение приводит к:

  • пересборке части графа зависимостей
  • пересозданию сервисных объектов
  • инвалидации кэшей

Это означает, что DI в системе является не статическим, а динамическим: зависимости могут пересоздаваться в runtime.

Плагины и внешние сервисы

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

  • пользовательские контролы
  • кастомные источники данных
  • сторонние слои визуализации

Такие компоненты внедряются в систему через публичное API карты и получают доступ к внутренним сервисам косвенно:

  • через map
  • через события
  • через доступ к слоям и источникам

Это создаёт ограниченную форму внешнего DI, где карта остаётся единственным точкой входа.

Особенности инверсии управления

Инверсия управления в Mapbox GL JS проявляется в том, что:

  • сервисы не вызываются напрямую пользователем
  • карта управляет жизненным циклом всех компонентов
  • рендеринг инициируется изменением состояния, а не внешним вызовом

Например:

  • изменение координат вызывает пересчёт Transform
  • это активирует Painter
  • который запрашивает данные у SourceCache
  • который обращается к worker-сервисам

Поток управления всегда идёт сверху вниз от Map, а не от отдельных модулей.

Ограничения и особенности модели DI

Несмотря на гибкость, такая архитектура имеет ряд особенностей:

  • скрытые зависимости через map усложняют анализ связей
  • service locator внутри Map может затруднять тестирование
  • динамическая пересборка сервисов влияет на предсказуемость состояния
  • worker-based DI добавляет асинхронность в граф зависимостей

Тем не менее такая модель обеспечивает высокую производительность и возможность работы с большими объёмами геоданных в браузере без блокировки UI-потока