Wrapper библиотеки

Wrapper-библиотека поверх API карт представляет собой слой абстракции, скрывающий прямое взаимодействие с глобальными объектами google.maps, упрощающий управление картой, маркерами, сервисами геокодирования и событиями, а также стандартизирующий интерфейс для приложения.

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


Архитектурная роль wrapper-слоя

Нативный API Google Maps JavaScript API предоставляет широкий набор классов: Map, Marker, InfoWindow, LatLng, Geocoder, DirectionsService. Однако прямое использование этих объектов приводит к нескольким проблемам:

  • сильная связанность с глобальным объектом google
  • разрозненная логика создания и управления сущностями
  • сложность мокирования в тестах
  • повторяющийся код инициализации
  • отсутствие единого стиля работы с сущностями карты

Wrapper решает эти проблемы через единый интерфейс:

const map = new MapWrapper({
  containerId: "map",
  center: { lat: 40.7128, lng: -74.0060 },
  zoom: 12
});

Базовая структура wrapper-библиотеки

Типичная структура wrapper-слоя включает несколько модулей:

  • CoreMap — управление экземпляром карты
  • MarkerManager — создание и контроль маркеров
  • OverlayManager — работа с InfoWindow и кастомными слоями
  • GeoService — геокодирование и reverse geocoding
  • RouteService — маршрутизация
  • EventBus — унифицированная система событий
  • Loader — динамическая загрузка API

Инициализация карты через wrapper

Прямой API требует работы с new google.maps.Map. Wrapper скрывает этот вызов:

class MapWrapper {
  constructor(options) {
    this.options = options;
    this.map = null;
  }

  async init() {
    await this.loadAPI();

    this.map = new google.maps.Map(
      document.getElementById(this.options.containerId),
      {
        center: this.options.center,
        zoom: this.options.zoom,
        mapTypeId: "roadmap"
      }
    );
  }

  loadAPI() {
    if (window.google && window.google.maps) return Promise.resolve();

    return new Promise((resolve) => {
      const script = document.createElement("script");
      script.src = `https://maps.googleapis.com/maps/api/js?key=${this.options.apiKey}`;
      script.onl oad = resolve;
      document.head.appendChild(script);
    });
  }
}

Wrapper обеспечивает контроль загрузки SDK, предотвращая повторные подключения и гонки инициализации.


Абстракция маркеров

Работа с маркерами в нативном API часто разрознена: создание, обновление позиции, удаление и обработка событий выполняются разными вызовами.

Wrapper централизует управление:

class MarkerManager {
  constructor(mapInstance) {
    this.map = mapInstance;
    this.markers = new Map();
  }

  add(id, position, options = {}) {
    const marker = new google.maps.Marker({
      position,
      map: this.map,
      ...options
    });

    this.markers.set(id, marker);
    return marker;
  }

  updatePosition(id, position) {
    const marker = this.markers.get(id);
    if (marker) marker.setPosition(position);
  }

  remove(id) {
    const marker = this.markers.get(id);
    if (marker) {
      marker.setMap(null);
      this.markers.delete(id);
    }
  }

  clear() {
    this.markers.forEach(m => m.setMap(null));
    this.markers.clear();
  }
}

Ключевая особенность wrapper-подхода — идентификация маркеров через id, а не через ссылки на объекты API.


Унификация событий

Google Maps JavaScript API использует собственную систему событий через google.maps.event.addListener, что усложняет интеграцию с архитектурой приложения.

Wrapper вводит единый EventBus:

class EventBus {
  constructor() {
    this.events = {};
  }

  on(event, handler) {
    if (!this.events[event]) this.events[event] = [];
    this.events[event].push(handler);
  }

  emit(event, payload) {
    (this.events[event] || []).forEach(h => h(payload));
  }
}

Интеграция с картой:

this.map.addListener("click", (e) => {
  this.eventBus.emit("map:click", {
    lat: e.latLng.lat(),
    lng: e.latLng.lng()
  });
});

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


Геокодирование через сервисный слой

Geocoder в нативном API возвращает результаты через callback, что усложняет композицию логики. Wrapper преобразует его в Promise-based API:

class GeoService {
  constructor() {
    this.geocoder = new google.maps.Geocoder();
  }

  geocode(address) {
    return new Promise((resolve, reject) => {
      this.geocoder.geocode({ address }, (results, status) => {
        if (status === "OK") resolve(results);
        else reject(status);
      });
    });
  }

  reverseGeocode(lat, lng) {
    return new Promise((resolve, reject) => {
      this.geocoder.geocode({ location: { lat, lng } }, (results, status) => {
        if (status === "OK") resolve(results);
        else reject(status);
      });
    });
  }
}

Это позволяет использовать async/await и выстраивать линейную бизнес-логику.


Управление состоянием карты

Wrapper часто включает слой состояния, который синхронизирует:

  • центр карты
  • масштаб
  • активные маркеры
  • выбранные объекты
class MapState {
  constructor() {
    this.state = {
      center: null,
      zoom: null,
      selectedMarker: null
    };
  }

  setCenter(center) {
    this.state.center = center;
  }

  setZoom(zoom) {
    this.state.zoom = zoom;
  }

  selectMarker(id) {
    this.state.selectedMarker = id;
  }

  getState() {
    return this.state;
  }
}

Wrapper синхронизирует состояние с картой:

this.map.addListener("center_changed", () => {
  this.state.setCenter(this.map.getCenter().toJSON());
});

Композиция сервисов

В зрелых wrapper-архитектурах каждый сервис изолирован, но управляется единым фасадом:

class MapsFacade {
  constructor(config) {
    this.map = new MapWrapper(config);
    this.markers = null;
    this.geo = null;
    this.events = new EventBus();
  }

  async init() {
    await this.map.init();

    this.markers = new MarkerManager(this.map.map);
    this.geo = new GeoService();
  }
}

Фасад скрывает сложность SDK и предоставляет единый интерфейс приложения.


Расширяемость wrapper-слоя

Wrapper над Google Maps JavaScript API часто проектируется как расширяемая система плагинов:

class PluginSystem {
  constructor() {
    this.plugins = [];
  }

  use(plugin) {
    this.plugins.push(plugin);
    plugin.init(this);
  }
}

Пример плагина:

const TrafficPlugin = {
  init(wrapper) {
    wrapper.map.map.setOptions({ trafficLayer: true });
  }
};

Такой подход позволяет добавлять функциональность без изменения ядра wrapper-библиотеки.


Обработка ошибок и устойчивость

Wrapper-слой централизует обработку ошибок SDK:

  • недоступность API ключа
  • превышение лимитов запросов
  • ошибки геокодирования
  • ошибки загрузки скрипта
try {
  await geo.geocode("New York");
} catch (e) {
  eventBus.emit("geo:error", e);
}

Это предотвращает распространение ошибок по бизнес-логике приложения.


Тестирование wrapper-слоя

Ключевая ценность wrapper заключается в тестируемости. Вместо мокирования глобального google.maps, тестируется собственный интерфейс:

class MockGeoService {
  geocode() {
    return Promise.resolve([{ formatted_address: "Mock Address" }]);
  }
}

Инъекция зависимостей позволяет изолировать тесты от внешнего API.


Типизация и контракт интерфейса

В TypeScript wrapper обычно описывается через строгие интерфейсы:

interface LatLng {
  lat: number;
  lng: number;
}

interface MapConfig {
  containerId: string;
  center: LatLng;
  zoom: number;
  apiKey: string;
}

Это снижает вероятность ошибок при работе с динамическим API JavaScript.


Производительность wrapper-архитектуры

Дополнительный слой абстракции влияет на производительность минимально, однако позволяет оптимизировать:

  • батчинг маркеров
  • дебаунс событий перемещения карты
  • ленивую загрузку сервисов
  • кэширование геокодирования
function debounce(fn, delay) {
  let t;
  return (...args) => {
    clearTimeout(t);
    t = setTimeout(() => fn(...args), delay);
  };
}

Итоговая роль wrapper-подхода

Wrapper над Google Maps JavaScript API формирует промежуточный слой между внешним SDK и внутренней архитектурой приложения, обеспечивая:

  • изоляцию зависимости от внешнего API
  • унификацию интерфейсов работы с картой
  • централизованное управление состоянием
  • расширяемость через плагины
  • тестируемость бизнес-логики
  • предсказуемую обработку ошибок