Рефакторинг legacy кода на JavaScript: продвинутый уровень

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Рефакторинг legacy кода на JavaScript: продвинутый уровень

Работа с унаследованным (legacy) кодом — неизбежная реальность для любого разработчика, особенно в сфере Node.js. Часто такой код представляет собой «болото» из глобальных переменных, вложенных callback'ов, функций длиной в сотни строк и полного отсутствия модульной структуры. Простые методы рефакторинга здесь не работают: требуется системный подход и знание продвинутых техник. В этой статье мы разберем стратегии, которые помогут превратить хаос в поддерживаемую архитектуру без остановки продакшена.

1. Анализ и картографирование зависимостей

Прежде чем менять код, его нужно понять. В legacy-проектах документация часто отсутствует или устарела. Первый шаг — построить карту зависимостей.

Инструменты:

  • Madge — утилита для визуализации зависимостей между модулями. Она покажет циклические зависимости и «мертвые» файлы.
  • ESLint с плагинами — поможет выявить неиспользуемые переменные, функции и импорты.
  • Деплои в изолированную среду — запустите тесты (если их нет, напишите хотя бы smoke-тесты) и запишите, какие модули загружаются.

Пример: Допустим, у нас есть файл legacy.js, который содержит 2000 строк и использует глобальную переменную config.

// legacy.js (до рефакторинга)
var config = {
    apiUrl: 'https://api.example.com',
    timeout: 5000
};

function fetchData(callback) {
    // 50 строк логики с вложенными if и try/catch
    // использует глобальный config
    if (config.timeout > 0) {
        // ...
    }
    callback(null, data);
}

Действие: С помощью Madge мы видим, что legacy.js импортируется в 15 других файлах. Это «божественный объект». Наша задача — разбить его на атомарные модули.

2. Стратегия «Strangler Fig» (Фиговый душитель)

Этот паттерн идеален для рефакторинга без остановки системы. Суть: мы не переписываем всё сразу, а постепенно заменяем части старой системы новыми, пока старая не умрёт сама.

Реализация в Node.js:

  • Создаём новый модуль с чистой архитектурой.
  • Пишем прокси-слой (или фасад), который перенаправляет вызовы к старому или новому модулю.
  • Постепенно переключаем потребителей на новый модуль.

Пример: Заменяем старый callback-стиль на Promise/async/await.

// Старый API (callback-based)
function getUser(id, callback) {
    // legacy код
    callback(null, { id: id, name: 'John' });
}

// Новый API (Promise-based)
async function getUserAsync(id) {
    // новая реализация с async/await
    const user = await database.findUser(id);
    return user;
}

// Прокси-фасад
async function getUserFacade(id) {
    if (USE_NEW_IMPLEMENTATION) {
        return getUserAsync(id);
    } else {
        return new Promise((resolve, reject) => {
            getUser(id, (err, user) => {
                if (err) reject(err);
                else resolve(user);
            });
        });
    }
}

Флаг USE_NEW_IMPLEMENTATION можно вынести в конфигурацию или feature toggle. Это позволяет включать новый код для части пользователей (canary release) или откатывать изменения без деплоя.

3. Модуляризация и изоляция побочных эффектов

Legacy-код часто страдает от скрытых побочных эффектов: функция меняет глобальное состояние, пишет в файл или вызывает внешний API, не возвращая результат. Рефакторинг таких функций — это выделение чистой логики.

Техника: «Извлечение функции» (Extract Function) с последующим внедрением зависимостей (Dependency Injection).

Проблемный код:

// Старый код: функция делает всё
function processOrder(orderId) {
    const order = db.query(`SELECT * FROM orders WHERE id = ${orderId}`); // побочный эффект
    const total = order.items.reduce((sum, item) => sum + item.price, 0);
    fs.writeFileSync('/tmp/log.txt', `Order ${orderId} processed`); // ещё один побочный эффект
    return total;
}

Рефакторинг: Разделяем на три модуля: работа с БД, расчёт суммы, логирование.

// orderRepository.js
async function getOrderById(orderId) {
    // чистая абстракция доступа к данным
    return await db.query('SELECT * FROM orders WHERE id = $1', [orderId]);
}

// orderCalculator.js
function calculateTotal(order) {
    // чистая функция, без побочных эффектов
    return order.items.reduce((sum, item) => sum + item.price, 0);
}

// logger.js
function logProcessing(orderId) {
    // изолированный побочный эффект
    fs.appendFileSync('/var/log/orders.log', `Order ${orderId} processed at ${Date.now()}\n`);
}

// Основной модуль — оркестратор
async function processOrder(orderId) {
    const order = await getOrderById(orderId);
    const total = calculateTotal(order);
    logProcessing(orderId);
    return total;
}

Теперь каждый модуль можно тестировать изолированно. calculateTotal — чистая функция, её легко покрыть unit-тестами. getOrderById и logProcessing можно замокать.

4. Миграция на TypeScript и строгую типизацию

Один из самых мощных шагов для улучшения legacy-кода — добавление типов. TypeScript не только ловит ошибки, но и служит живой документацией. Однако в большом проекте внедрять его нужно постепенно.

Стратегия:

  1. allowJs: true — TypeScript может работать с .js файлами. Начните с добавления tsconfig.json.
  2. checkJs: true — включите проверку типов для .js файлов. Сначала будет много ошибок, но это покажет проблемные места.
  3. Добавление JSDoc аннотаций — вместо полного переписывания файлов, добавьте типы через JSDoc. Это даст автодополнение и проверку без компиляции.
  4. Блоги

    Рекомендаций по книгам