Паттерны организации кода

Организация кода в приложениях на Deck.gl напрямую влияет на масштабируемость, производительность и удобство сопровождения. При увеличении количества слоёв, источников данных и интерактивных сценариев монолитная структура быстро становится узким местом. Базовый принцип — разделение ответственности между слоями, данными, состоянием камеры и логикой взаимодействия.

Типичная архитектура строится вокруг следующих сущностей:

  • инициализация Deck или DeckGL
  • конфигурация слоёв (layers)
  • управление данными (fetch, transform, cache)
  • управление состоянием (viewState)
  • обработчики событий (hover, click, pick)

Разделение этих частей в разные модули позволяет избежать переполнения основного файла и упростить тестирование.

Разделение слоёв по доменной логике

Один из ключевых паттернов — группировка слоёв по смысловым доменам. Вместо хранения всех слоёв в одном массиве они распределяются по отдельным модулям.

Пример структуры:

/layers
  roadsLayer.js
  buildingsLayer.js
  vehiclesLayer.js
  weatherLayer.js

Каждый модуль экспортирует фабрику слоя:

import { GeoJsonLayer } from '@deck.gl/layers';

export function createRoadsLayer(data) {
  return new GeoJsonLayer({
    id: 'roads-layer',
    data,
    stroked: true,
    getLineColor: [120, 120, 120],
    getLineWidth: 2
  });
}

Такой подход даёт несколько преимуществ:

  • изоляция конфигурации визуализации
  • упрощение переиспользования слоёв
  • независимая отладка каждого слоя
  • возможность динамической подгрузки модулей

Фабрики слоёв вместо прямого создания

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

Фабрика может принимать не только данные, но и параметры конфигурации:

export function createBuildingsLayer(data, options = {}) {
  return new ColumnLayer({
    id: 'buildings',
    data,
    diskResolution: options.resolution || 12,
    radius: options.radius || 50,
    getPosition: d => d.position,
    getElevation: d => d.height,
    getFillColor: d => d.color || [200, 200, 200]
  });
}

Ключевая идея — слой становится чистой функцией от данных и конфигурации.

Композиция слоёв

В сложных сценах Deck.gl слои редко существуют изолированно. Они комбинируются в композиции, где каждый слой отвечает за свой аспект визуализации.

Организация композиции часто реализуется через функцию сборки:

import { createRoadsLayer } from './layers/roadsLayer';
import { createBuildingsLayer } from './layers/buildingsLayer';
import { createVehiclesLayer } from './layers/vehiclesLayer';

export function createLayers(state) {
  return [
    createRoadsLayer(state.roads),
    createBuildingsLayer(state.buildings, state.buildingOptions),
    createVehiclesLayer(state.vehicles, state.time)
  ];
}

Такая структура позволяет:

  • централизованно управлять порядком слоёв
  • контролировать зависимости между слоями
  • легко добавлять или отключать функциональность

Разделение данных и визуализации

Частая ошибка — смешивание загрузки данных и визуализации внутри слоя. Более устойчивый подход заключается в выделении слоя данных отдельно от слоя представления.

Пример организации:

/data
  fetchRoads.js
  transformBuildings.js
  normalizeVehicles.js

/layers
  roadsLayer.js
  buildingsLayer.js
  vehiclesLayer.js

Функции обработки данных:

export async function fetchRoads() {
  const response = await fetch('/api/roads');
  const json = await response.json();

  return json.features.map(f => ({
    coordinates: f.geometry.coordinates,
    type: f.properties.type
  }));
}

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

Слой как функция состояния

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

Организация через чистые функции состояния:

export function createFilteredLayer(data, filters) {
  return new ScatterplotLayer({
    id: 'points',
    data: data.filter(d => d.value > filters.minValue),
    getPosition: d => d.position,
    getRadius: d => d.value
  });
}

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

Централизация viewState

Состояние камеры (viewState) часто становится источником дублирования логики. Для больших приложений используется централизованный модуль управления видом.

/state
  viewStateManager.js
export function createViewStateManager(initialState) {
  let state = initialState;

  function setViewState(newState) {
    state = {
      ...state,
      ...newState
    };
  }

  function getViewState() {
    return state;
  }

  return {
    setViewState,
    getViewState
  };
}

Это позволяет:

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

Организация интерактивности

Интерактивность в Deck.gl строится вокруг picking-системы. При усложнении логики события лучше выносить в отдельный модуль.

/interactions
  hoverHandler.js
  clickHandler.js
  selectionManager.js

Пример обработки hover:

export function onHover(info, state) {
  if (info.object) {
    return {
      hoveredId: info.object.id,
      tooltip: {
        x: info.x,
        y: info.y,
        content: info.object.name
      }
    };
  }

  return {
    hoveredId: null,
    tooltip: null
  };
}

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

Паттерн «слой-адаптер»

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

export function adaptApiResponse(response) {
  return response.items.map(item => ({
    position: [item.lon, item.lat],
    value: Number(item.metric),
    timestamp: item.time
  }));
}

Это снижает связанность между API и визуализацией.

Кэширование вычисляемых данных

Deck.gl часто работает с большими массивами, и повторные вычисления становятся дорогими. Используется кэширование промежуточных результатов.

const cache = new Map();

export function getProcessedData(rawData) {
  const key = rawData.length;

  if (cache.has(key)) {
    return cache.get(key);
  }

  const processed = rawData.map(d => ({
    position: d.position,
    weight: d.value * 2
  }));

  cache.set(key, processed);

  return processed;
}

В более сложных сценариях ключом становится hash данных или комбинация параметров фильтрации.

Слоистая архитектура состояния приложения

При росте приложения удобно разделять состояние на уровни:

  • глобальное состояние (камера, тема)
  • доменное состояние (данные объектов)
  • локальное состояние слоя (hover, selection)

Каждый уровень управляется отдельно и не смешивается с другими.

Пример структуры:

/state
  globalState.js
  dataState.js
  layerState.js

Динамическая регистрация слоёв

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

const layerRegistry = new Map();

export function registerLayer(name, factory) {
  layerRegistry.set(name, factory);
}

export function buildLayers(context) {
  return Array.from(layerRegistry.values()).map(factory =>
    factory(context)
  );
}

Такой подход полезен для:

  • плагинной архитектуры
  • модульных карт
  • A/B конфигураций визуализации

Изоляция побочных эффектов

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

Пример неправильного подхода:

new ScatterplotLayer({
  data,
  onHover: info => {
    fetch('/log', { method: 'POST', body: JSON.stringify(info) });
  }
});

Корректная организация:

function handleHover(info) {
  logEvent(info);
  updateTooltip(info);
}

new ScatterplotLayer({
  data,
  onHover: handleHover
});

Разделение конфигурации и исполнения

Конфигурация слоёв может храниться отдельно от кода. Это особенно важно для больших визуализационных систем.

/config
  layersConfig.js
  mapConfig.js
export const roadsConfig = {
  stroked: true,
  lineWidthMinPixels: 1,
  getLineColor: [80, 80, 80]
};

Исполнение конфигурации:

import { GeoJsonLayer } from '@deck.gl/layers';
import { roadsConfig } from '../config/layersConfig';

export function createRoadsLayer(data) {
  return new GeoJsonLayer({
    id: 'roads',
    data,
    ...roadsConfig
  });
}

Переиспользуемые композиционные функции

Для уменьшения дублирования создаются функции-компоновщики:

export function withHighlight(layer, conditionFn) {
  return layer.clone({
    getFillColor: d =>
      conditionFn(d) ? [255, 0, 0] : layer.props.getFillColor(d)
  });
}

Такой паттерн позволяет расширять поведение слоёв без изменения исходного кода.

Организация проекта при масштабировании

При росте приложения структура часто эволюционирует в следующую форму:

/src
  /layers
  /data
  /state
  /interactions
  /services
  /utils
  /config
  deckInstance.js
  createScene.js

deckInstance.js отвечает за инициализацию Deck.gl, а createScene.js — за композицию всех слоёв и состояния.

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