Рефакторинг legacy кода на JavaScript: как превратить хаос в порядок
Работа с legacy кодом — неизбежная часть жизни разработчика. Рано или поздно каждый сталкивается с проектом, написанным на устаревшем JavaScript, где переменные называются a1, temp или data, а логика погребена под сотнями строк в одном файле. Рефакторинг такого кода не просто улучшает читаемость — он снижает стоимость поддержки, ускоряет добавление новых функций и уменьшает количество багов.
В этой статье мы разберем, как подходить к рефакторингу legacy JavaScript кода в среде Node.js, какие инструменты использовать и как не сломать работающую систему.
Почему legacy код опасен и почему его нужно рефакторить
Legacy код (от англ. legacy — наследие) — это код, который уже работает в продакшене, но написан с нарушением современных стандартов, без тестов и с плохой архитектурой. Основные проблемы такого кода:
- Сложность понимания. Отсутствие модульности, длинные функции, магические числа.
- Высокая связность. Изменение одной части ломает другую, не связанную на первый взгляд.
- Отсутствие тестов. Любое изменение — риск.
- Устаревшие паттерны. Использование
varвместоconst/let, callback hell, отсутствие async/await.
Рефакторинг — это не переписывание всего с нуля. Это постепенное улучшение кода без изменения его внешнего поведения. Цель — сделать код чище, безопаснее и легче для поддержки.
Стратегия рефакторинга: с чего начать
Перед тем как менять код, нужно разработать план. Хаотичные правки только усугубят ситуацию. Вот проверенная стратегия:
- Задокументируйте текущее поведение. Запишите, что делает каждая функция, какие входные данные ожидает и что возвращает.
- Напишите тесты. Даже если нет юнит-тестов, напишите интеграционные тесты, которые проверяют ключевые сценарии. Используйте
jestилиmocha. - Изолируйте изменения. Работайте с одним модулем или функцией за раз. Не пытайтесь рефакторить всё сразу.
- Применяйте малые шаги. Одно изменение — один коммит. Если что-то сломалось, легко откатиться.
Пример: у вас есть функция, которая обрабатывает данные из файла и сохраняет результат. Сначала напишите тест, который проверяет, что при входном файле A получается выходной файл B. Затем рефакторьте внутренности.
Типичные проблемы legacy JavaScript и их решения
1. Callback hell → async/await
Одна из самых частых проблем в старом Node.js коде — вложенные callback'и. Они делают код нечитаемым и трудноотлаживаемым.
Плохой код:
function getUserData(userId, callback) {
db.query('SELECT * FROM users WHERE id = ?', [userId], function(err, user) {
if (err) return callback(err);
db.query('SELECT * FROM orders WHERE user_id = ?', [user.id], function(err, orders) {
if (err) return callback(err);
callback(null, { user, orders });
});
});
}
После рефакторинга:
async function getUserData(userId) {
const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);
const orders = await db.query('SELECT * FROM orders WHERE user_id = ?', [user.id]);
return { user, orders };
}
Код стал короче, понятнее и легче для отладки. Ошибки обрабатываются через try/catch.
2. Глобальные переменные и мутация
В legacy коде часто встречаются глобальные переменные, которые изменяются в разных частях программы. Это приводит к неожиданным побочным эффектам.
Плохой код:
let config = { port: 3000 };
function setPort(newPort) {
config.port = newPort;
}
function startServer() {
console.log('Starting on port', config.port);
}
После рефакторинга — используем замыкания или классы:
function createConfig(initial) {
let config = { ...initial };
return {
get(key) { return config[key]; },
set(key, value) { config[key] = value; },
startServer() {
console.log('Starting on port', config.port);
}
};
}
const appConfig = createConfig({ port: 3000 });
appConfig.startServer();
Теперь состояние изолировано, и мутация контролируется через методы.
3. Магические числа и строки
Числа и строки, разбросанные по коду без пояснений, — классика legacy. Их нужно выносить в константы.
Плохой код:
if (status === 3) {
sendEmail();
}
После рефакторинга:
const STATUS = {
ACTIVE: 1,
PENDING: 2,
CANCELLED: 3
};
if (status === STATUS.CANCELLED) {
sendEmail();
}
Теперь понятно, что за статус проверяется. Если значение изменится, правка будет в одном месте.
4. Длинные функции и дублирование
Функции на 200+ строк, которые делают всё сразу, — верный признак необходимости рефакторинга. Разбивайте их на мелкие, каждая из которых отвечает за одно действие.
Плохой код:
function processOrder(order) {
// 50 строк валидации
// 30 строк расчета скидки
// 40 строк отправки email
// 20 строк обновления БД
}
После рефакторинга:
function validateOrder(order) { /* ... */ }
function calculateDiscount(order) { /* ... */ }
function sendOrderEmail(order) { /* ... */ }
function updateDatabase(order) { /* ... */ }
async function processOrder(order) {
validateOrder(order);
const discount = calculateDiscount(order);
await sendOrderEmail(order);
await updateDatabase(order);
}
Каждая функция теперь легко тестируется и переиспользуется.