Рефакторинг legacy кода на JavaScript: пошаговое руководство

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

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

Начать курс

Введение: почему рефакторинг legacy кода — это необходимость

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

В этой статье мы разберем, как подойти к рефакторингу legacy кода на JavaScript (Node.js) безопасно и эффективно. Вы узнаете о ключевых этапах: анализе, разбиении на модули, замене устаревших конструкций и тестировании. Все примеры написаны на современном JavaScript (ES6+) и ориентированы на среду Node.js.

Шаг 1: Анализ и понимание legacy кода

Прежде чем менять код, его нужно понять. Legacy код часто написан в стиле «спагетти»: функции длиной в сотни строк, глобальные переменные и смешение логики. Начните с аудита.

1.1. Создайте карту зависимостей

Используйте инструменты вроде madge или dependency-cruiser для визуализации связей между модулями. Это поможет выявить циклические зависимости и «божественные объекты».

// Пример: установка madge
npm install -g madge
madge --image graph.svg src/

1.2. Определите «зоны риска»

Найдите участки кода, которые чаще всего меняются или вызывают ошибки. Используйте историю коммитов в Git:

git log --oneline --follow src/legacyModule.js

Также обратите внимание на:

  • Функции длиннее 50 строк
  • Глобальные переменные (например, var без let/const)
  • Callback hell (вложенные асинхронные вызовы)

1.3. Напишите тесты для существующего поведения

Без тестов рефакторинг — это бомба замедленного действия. Используйте jest или mocha для покрытия ключевых функций. Тесты должны проверять текущее поведение, а не «идеальное».

// Пример теста для legacy функции
const sum = require('./legacyModule').sum;
test('sum adds two numbers', () => {
  expect(sum(1, 2)).toBe(3);
});

Шаг 2: Постепенное улучшение структуры

Не пытайтесь переписать всё сразу. Используйте стратегию «Strangler Fig» — заменяйте части кода постепенно, сохраняя обратную совместимость.

2.1. Разделите монолит на модули

Выделите логически связанные блоки в отдельные файлы. Например, вынесите работу с базой данных в db.js, а обработку запросов — в handlers.js.

// До: один файл на 500 строк
function handleRequest(req, res) {
  // 100 строк логики
  // 50 строк работы с БД
  // 30 строк форматирования ответа
}

// После: разбивка на модули
// db.js
exports.getUser = (id) => { /* ... */ };

// handlers.js
const db = require('./db');
exports.handleRequest = (req, res) => {
  const user = db.getUser(req.params.id);
  // форматирование ответа
};

2.2. Замените var на let/const

Это простой, но важный шаг. var имеет функциональную область видимости, что часто приводит к багам. Используйте let для переменных и const для констант.

// Было
var x = 10;
if (true) {
  var x = 20; // переопределяет внешнюю переменную
}

// Стало
let x = 10;
if (true) {
  let x = 20; // локальная переменная
}

Шаг 3: Модернизация асинхронного кода

Legacy код на Node.js часто страдает от «callback hell». Замените вложенные колбэки на промисы или async/await.

3.1. Преобразование callback в промисы

Используйте util.promisify для функций, следующих стилю Node.js (первый аргумент — ошибка).

const fs = require('fs');
const { promisify } = require('util');
const readFile = promisify(fs.readFile);

// Было
fs.readFile('data.txt', 'utf8', (err, data) => {
  if (err) throw err;
  console.log(data);
});

// Стало
async function readData() {
  try {
    const data = await readFile('data.txt', 'utf8');
    console.log(data);
  } catch (err) {
    console.error(err);
  }
}

3.2. Упрощение цепочек промисов

Если в коде есть цепочки .then(), замените их на async/await для читаемости.

// Было
fetchData()
  .then(result => process(result))
  .then(final => save(final))
  .catch(err => console.error(err));

// Стало
async function handleData() {
  try {
    const result = await fetchData();
    const final = await process(result);
    await save(final);
  } catch (err) {
    console.error(err);
  }
}

Шаг 4: Тестирование и безопасный деплой

После каждого изменения запускайте тесты. Используйте git stash для изоляции изменений и CI/CD для автоматической проверки.

4.1. Добавьте линтер и форматтер

Настройте ESLint и Prettier для поддержания единого стиля кода. Это предотвратит появление нового «грязного» кода.

// .eslintrc.json
{
  "extends": "eslint:recommended",
  "rules": {
    "no-var": "error",
    "prefer-const": "warn"
  }
}

4.2. Используйте feature toggles

Для крупных изменений внедряйте фичу под флагом. Это позволит откатить изменения без полного деплоя.

const isNewFeatureEnabled = process.env.NEW_FEATURE === 'true';

if (isNewFeatureEnabled) {
  // новый код
} else {
  // legacy код
}

Заключение

Блоги

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