Переписывание кода

В процессе развития картографического приложения код редко остаётся неизменным. Появляются новые требования, изменяется структура данных, обновляются версии библиотек, возникают задачи повышения производительности и читаемости. Переписывание кода (refactoring и code rewriting) представляет собой процесс изменения внутренней структуры программы без изменения её внешнего поведения.

Для проектов на Mapbox GL JS переписывание особенно важно по нескольким причинам:

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

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


Признаки необходимости переписывания

Некоторые признаки напрямую указывают на то, что код требует переработки.

Повторение одинаковых конструкций

Плохой пример:

map.addLayer({
    id: 'cities',
    type: 'circle',
    source: 'places',
    filter: ['==', ['get', 'type'], 'city']
});

map.addLayer({
    id: 'villages',
    type: 'circle',
    source: 'places',
    filter: ['==', ['get', 'type'], 'village']
});

map.addLayer({
    id: 'towns',
    type: 'circle',
    source: 'places',
    filter: ['==', ['get', 'type'], 'town']
});

Одинаковая структура слоя повторяется несколько раз.

После переписывания:

function addSettlementLayer(id, type) {
    map.addLayer({
        id,
        type: 'circle',
        source: 'places',
        filter: ['==', ['get', 'type'], type]
    });
}

addSettlementLayer('cities', 'city');
addSettlementLayer('villages', 'village');
addSettlementLayer('towns', 'town');

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


Слишком длинные функции

Плохой пример:

function initializeMap() {
    map.addSource(...);
    map.addLayer(...);
    map.on(...);
    map.on(...);
    map.on(...);
    map.on(...);
    map.on(...);
    map.on(...);
}

Подобные функции быстро разрастаются до нескольких сотен строк.

После переписывания:

function initializeSources() {
    // Источники
}

function initializeLayers() {
    // Слои
}

function initializeEvents() {
    // События
}

function initializeMap() {
    initializeSources();
    initializeLayers();
    initializeEvents();
}

Каждый блок ответственности отделён от остальных.


Выделение логики работы со слоями

Одной из наиболее распространённых задач является управление слоями.

Исходный вариант

map.addLayer({
    id: 'roads',
    type: 'line',
    source: 'transport',
    paint: {
        'line-color': '#ff0000',
        'line-width': 2
    }
});

map.addLayer({
    id: 'railways',
    type: 'line',
    source: 'transport',
    paint: {
        'line-color': '#0000ff',
        'line-width': 2
    }
});

Структуры практически идентичны.

Переработанный вариант

function createLineLayer(id, color) {
    return {
        id,
        type: 'line',
        source: 'transport',
        paint: {
            'line-color': color,
            'line-width': 2
        }
    };
}

map.addLayer(createLineLayer('roads', '#ff0000'));
map.addLayer(createLineLayer('railways', '#0000ff'));

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


Переписывание обработчиков событий

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

Неудачная реализация

map.on('click', 'cities', function(e) {
    console.log(e.features[0]);
});

map.on('click', 'villages', function(e) {
    console.log(e.features[0]);
});

map.on('click', 'towns', function(e) {
    console.log(e.features[0]);
});

Код дублируется.

Улучшенная версия

function handleSettlementClick(event) {
    console.log(event.features[0]);
}

['cities', 'villages', 'towns']
    .forEach(layer => {
        map.on('click', layer, handleSettlementClick);
    });

Логика сосредоточена в одном месте.


Переход от жёстко заданных значений к конфигурациям

Жёстко прописанные параметры усложняют поддержку.

Плохой пример:

map.setPaintProperty(
    'cities',
    'circle-color',
    '#00ff00'
);

map.setPaintProperty(
    'cities',
    'circle-radius',
    8
);

После переписывания:

const CITY_STYLE = {
    color: '#00ff00',
    radius: 8
};

map.setPaintProperty(
    'cities',
    'circle-color',
    CITY_STYLE.color
);

map.setPaintProperty(
    'cities',
    'circle-radius',
    CITY_STYLE.radius
);

Теперь изменение стиля выполняется в одном месте.


Вынос GeoJSON-логики

Часто встречается ситуация, когда данные создаются непосредственно внутри функций.

До переработки

map.addSource('places', {
    type: 'geojson',
    data: {
        type: 'FeatureCollection',
        features: [...]
    }
});

После переработки

const placesData = {
    type: 'FeatureCollection',
    features: [...]
};

map.addSource('places', {
    type: 'geojson',
    data: placesData
});

Логика отображения отделяется от логики хранения данных.


Создание фабрик слоёв

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

Исходный код

map.addLayer({
    id: 'restaurants',
    type: 'symbol',
    source: 'pois'
});

map.addLayer({
    id: 'hotels',
    type: 'symbol',
    source: 'pois'
});

map.addLayer({
    id: 'museums',
    type: 'symbol',
    source: 'pois'
});

После переписывания

function createSymbolLayer(id) {
    return {
        id,
        type: 'symbol',
        source: 'pois'
    };
}

[
    'restaurants',
    'hotels',
    'museums'
].forEach(layer => {
    map.addLayer(createSymbolLayer(layer));
});

Переписывание асинхронного кода

Современные версии JavaScript позволяют заменить вложенные цепочки вызовов более читаемыми конструкциями.

Старый подход

fetch('/api/places')
    .then(response => response.json())
    .then(data => {
        map.getSource('places')
            .setData(data);
    })
    .catch(error => {
        console.error(error);
    });

Новый подход

async function loadPlaces() {
    try {
        const response = await fetch('/api/places');
        const data = await response.json();

        map.getSource('places')
            .setData(data);
    }
    catch (error) {
        console.error(error);
    }
}

Код становится линейным и легче воспринимается.


Разделение карты на модули

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

Пример структуры проекта:

src/
│
├── map/
│   ├── map.js
│   ├── layers.js
│   ├── sources.js
│   └── events.js
│
├── services/
│   ├── api.js
│   └── geojson.js
│
└── app.js

Такое разделение значительно упрощает сопровождение.


Переписывание логики фильтрации

Неудачный вариант

map.setFilter(
    'cities',
    [
        'all',
        ['==', ['get', 'country'], 'France'],
        ['>', ['get', 'population'], 500000]
    ]
);

Более поддерживаемый вариант

const cityFilter = [
    'all',
    ['==', ['get', 'country'], 'France'],
    ['>', ['get', 'population'], 500000]
];

map.setFilter(
    'cities',
    cityFilter
);

Фильтр можно переиспользовать и изменять независимо от вызова API.


Замена императивного подхода декларативным

До переписывания

if (type === 'city') {
    map.setPaintProperty(
        'places',
        'circle-color',
        '#ff0000'
    );
}

if (type === 'village') {
    map.setPaintProperty(
        'places',
        'circle-color',
        '#00ff00'
    );
}

if (type === 'town') {
    map.setPaintProperty(
        'places',
        'circle-color',
        '#0000ff'
    );
}

После переписывания

const colors = {
    city: '#ff0000',
    village: '#00ff00',
    town: '#0000ff'
};

map.setPaintProperty(
    'places',
    'circle-color',
    colors[type]
);

Количество условий сокращается, а код становится расширяемым.


Переписывание логики всплывающих окон

Исходный код

map.on('click', 'cities', function(e) {

    const popup = new mapboxgl.Popup();

    popup
        .setLngLat(e.lngLat)
        .setHTML(
            '<h3>' +
            e.features[0].properties.name +
            '</h3>'
        )
        .addTo(map);

});

После переработки

function showPopup(lngLat, html) {
    new mapboxgl.Popup()
        .setLngLat(lngLat)
        .setHTML(html)
        .addTo(map);
}

map.on('click', 'cities', e => {

    const name =
        e.features[0].properties.name;

    showPopup(
        e.lngLat,
        `<h3>${name}</h3>`
    );

});

Функция становится универсальной для любых слоёв.


Оптимизация обновления данных

Неэффективный вариант

setInterval(() => {

    fetch('/api/data')
        .then(response => response.json())
        .then(data => {

            map.removeSource('places');

            map.addSource('places', {
                type: 'geojson',
                data
            });

        });

}, 5000);

Источник пересоздаётся каждый раз.

Переработанный вариант

setInterval(async () => {

    const response =
        await fetch('/api/data');

    const data =
        await response.json();

    map.getSource('places')
        .setData(data);

}, 5000);

Количество операций значительно уменьшается.


Переписывание стилей на основе выражений

Множество отдельных условий можно заменить выражениями Mapbox.

Старый подход

if (population > 1000000) {
    color = 'red';
}
else if (population > 500000) {
    color = 'orange';
}
else {
    color = 'green';
}

Новый подход

'circle-color': [
    'step',
    ['get', 'population'],
    'green',
    500000,
    'orange',
    1000000,
    'red'
]

Логика переносится непосредственно в стиль слоя.


Переписывание кода при миграции версий

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

Основные направления миграции:

  1. Удаление устаревших методов.
  2. Замена deprecated API.
  3. Перевод кода на современные конструкции JavaScript.
  4. Пересмотр структуры модулей.
  5. Унификация именования слоёв и источников.
  6. Оптимизация работы с GeoJSON.

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


Стратегия безопасного переписывания

Эффективное переписывание обычно выполняется поэтапно:

  1. Выделение проблемного участка.
  2. Покрытие критической логики тестами.
  3. Создание новой реализации.
  4. Проверка визуального результата на карте.
  5. Замена старого кода новым.
  6. Удаление неиспользуемых конструкций.

Для проектов на Mapbox GL JS особенно важно после каждого этапа проверять:

  • корректность отображения слоёв;
  • работу фильтров;
  • функционирование событий;
  • загрузку GeoJSON-данных;
  • производительность масштабирования и перемещения карты;
  • корректность всплывающих окон и элементов интерфейса.

Систематическое переписывание кода позволяет поддерживать картографическое приложение в предсказуемом, расширяемом и производительном состоянии даже при длительном развитии проекта и значительном росте объёма функциональности.