Введение: почему рефакторинг 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 код
}
Заключение
Блоги
Рекомендаций по книгам
Читайте также