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

Разделение логики и представления в картографических веб-приложениях становится критически важным по мере роста сложности интерактивных карт, увеличения числа слоёв, источников данных и пользовательских сценариев. В экосистеме Mapbox GL JS это разделение позволяет удерживать управляемость кода, минимизировать связанность компонентов и обеспечить предсказуемость поведения карты при изменении состояния приложения.

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


Роль представления в Mapbox GL JS

Представление в контексте Mapbox GL JS ограничивается задачами визуализации:

  • отображение тайлов и векторных данных;
  • рендеринг слоёв (layers);
  • применение стилей (style specification);
  • визуальные эффекты (interpolation, transitions);
  • обработка низкоуровневых событий карты (click, move, zoom).

Mapbox GL JS фактически выступает как декларативный движок рендеринга, где состояние визуализации описывается через стиль, источники данных и слои. Это создаёт естественное разделение: карта не должна содержать бизнес-логику, она лишь отражает входное состояние.

Пример роли представления:

map.addSource('cities', {
  type: 'geojson',
  data: '/api/cities'
});

map.addLayer({
  id: 'cities-layer',
  type: 'circle',
  source: 'cities',
  paint: {
    'circle-radius': 6,
    'circle-color': '#ff5500'
  }
});

В данном случае карта лишь отображает источник cities, не определяя, откуда он берётся и как формируется.


Логика приложения: выделение доменного слоя

Логика приложения включает:

  • получение и агрегацию данных;
  • фильтрацию и трансформацию GeoJSON;
  • вычисление состояний (выбранные объекты, активные фильтры);
  • взаимодействие с API;
  • управление пользовательскими сценариями.

Эта часть не должна зависеть от API карты. Она работает с абстракциями данных, а не с конкретными слоями или стилями.

Пример логического слоя:

class CityService {
  async fetchCities() {
    const response = await fetch('/api/cities');
    const data = await response.json();

    return data.features.map(feature => ({
      id: feature.id,
      name: feature.properties.name,
      coordinates: feature.geometry.coordinates
    }));
  }
}

Здесь отсутствует любая привязка к Mapbox GL JS. Это позволяет переиспользовать сервис в других визуализациях.


Разрыв зависимости через промежуточное состояние

Одним из ключевых механизмов разделения становится введение промежуточного состояния приложения (state layer). Оно выступает мостом между логикой и представлением.

Состояние может включать:

  • текущий набор данных;
  • активные фильтры;
  • выбранные элементы;
  • параметры отображения карты.

Пример состояния:

const state = {
  cities: [],
  selectedCityId: null,
  filters: {
    populationMin: 100000
  }
};

Изменение состояния не должно напрямую манипулировать картой. Вместо этого оно должно инициировать синхронизацию.


Слой синхронизации: мост между логикой и картой

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

  • обновление источников данных;
  • изменение фильтров слоёв;
  • управление видимостью;
  • изменение стилей.

Пример:

function syncCitiesLayer(map, state) {
  const filtered = state.cities.filter(city =>
    city.population >= state.filters.populationMin
  );

  const geojson = {
    type: 'FeatureCollection',
    features: filtered.map(city => ({
      type: 'Feature',
      geometry: {
        type: 'Point',
        coordinates: city.coordinates
      },
      properties: {
        id: city.id
      }
    }))
  };

  const source = map.getSource('cities');
  source.setData(geojson);
}

Этот слой не содержит бизнес-логики фильтрации — она может быть вынесена отдельно, но здесь показан принцип передачи уже подготовленных данных в карту.


Разделение через архитектуру MVC

При использовании MVC-подхода:

  • Model — данные и бизнес-логика;
  • View — карта Mapbox GL JS;
  • Controller — обработка событий и синхронизация.

Контроллер связывает пользовательские действия с изменением состояния.

class MapController {
  constructor(map, service) {
    this.map = map;
    this.service = service;
  }

  async init() {
    const cities = await this.service.fetchCities();
    this.updateState({ cities });
    this.render();
  }

  updateState(partial) {
    this.state = { ...this.state, ...partial };
  }

  render() {
    syncCitiesLayer(this.map, this.state);
  }
}

Контроллер не содержит деталей рендеринга, он только координирует взаимодействие.


Использование событий карты как входного слоя управления

Mapbox GL JS предоставляет богатую систему событий, однако их обработка не должна смешиваться с логикой рендера.

Типичный подход:

  • события карты преобразуются в действия;
  • действия обновляют состояние;
  • состояние синхронизируется с картой.
map.on('click', 'cities-layer', (e) => {
  const cityId = e.features[0].properties.id;
  controller.selectCity(cityId);
});

Контроллер:

selectCity(id) {
  this.state.selectedCityId = id;
  this.render();
}

Таким образом, карта не принимает решений — она лишь сообщает о событиях.


Инкапсуляция работы со слоями

Работа со слоями в Mapbox GL JS часто становится источником тесной связанности кода. Разделение достигается через слой абстракции:

class CitiesLayer {
  constructor(map) {
    this.map = map;
    this.sourceId = 'cities';
    this.layerId = 'cities-layer';
  }

  setData(data) {
    this.map.getSource(this.sourceId).setData(data);
  }

  setVisibility(visible) {
    this.map.setLayoutProperty(
      this.layerId,
      'visibility',
      visible ? 'visible' : 'none'
    );
  }
}

Такой подход позволяет изолировать API Mapbox GL JS от остальной логики приложения.


Изоляция стилей от бизнес-логики

Стили в Mapbox GL JS часто ошибочно рассматриваются как часть логики. Однако они должны оставаться исключительно частью представления.

Неправильный подход:

if (city.population > 1000000) {
  map.setPaintProperty('cities-layer', 'circle-color', 'red');
}

Правильный подход:

  • бизнес-логика вычисляет категорию;
  • представление отображает категорию.
const enriched = cities.map(city => ({
  ...city,
  category: city.population > 1000000 ? 'large' : 'small'
}));
map.setPaintProperty('cities-layer', 'circle-color', [
  'match',
  ['get', 'category'],
  'large', '#ff0000',
  'small', '#00aaff',
  '#cccccc'
]);

Таким образом, карта не принимает решений, а лишь отображает заранее вычисленные признаки.


Управление состоянием как центральный элемент архитектуры

При росте сложности приложений на Mapbox GL JS состояние становится центральной точкой управления:

  • единый источник правды;
  • предсказуемость обновлений;
  • упрощение синхронизации;
  • снижение связанности компонентов.

Типовая структура состояния:

const state = {
  map: {
    center: [0, 0],
    zoom: 3
  },
  data: {
    cities: [],
    routes: []
  },
  ui: {
    selected: null,
    filters: {}
  }
};

Любое изменение состояния инициирует пересборку представления, но не прямое вмешательство в карту.


Поток данных: однонаправленная модель

Однонаправленный поток данных снижает количество ошибок синхронизации:

  1. Получение данных;
  2. Обновление состояния;
  3. Преобразование состояния в GeoJSON;
  4. Обновление источников Mapbox GL JS;
  5. Рендеринг карты.

Этот поток исключает сценарии, где карта напрямую модифицирует состояние приложения.


Изоляция внешних API и адаптерный слой

API Mapbox может изменяться, поэтому прямое использование его в бизнес-логике создаёт хрупкость системы.

Решение — адаптер:

class MapAdapter {
  constructor(map) {
    this.map = map;
  }

  updatePoints(sourceId, geojson) {
    this.map.getSource(sourceId).setData(geojson);
  }

  setLayerVisibility(layerId, visible) {
    this.map.setLayoutProperty(
      layerId,
      'visibility',
      visible ? 'visible' : 'none'
    );
  }
}

Бизнес-логика работает с адаптером, а не с Mapbox GL JS напрямую.


Разделение ответственности при масштабировании приложений

При увеличении количества слоёв, источников и интерактивных элементов становится критичным:

  • избегать прямых вызовов map.* в бизнес-логике;
  • централизовать обновление источников;
  • использовать состояние как единую точку координации;
  • выделять сервисы данных отдельно от визуализации;
  • ограничивать роль карты исключительно рендерингом.

Такой подход обеспечивает устойчивую архитектуру даже при сложных сценариях: динамические фильтры, real-time данные, мультилэйерные визуализации и взаимодействие с внешними API.