Введение: почему legacy код — это не приговор
Каждый разработчик рано или поздно сталкивается с legacy кодом на JavaScript. Это могут быть монолитные скрипты на Node.js, написанные "на коленке" 5 лет назад, или запутанные callback-функции, которые боятся трогать даже senior-разработчики. Однако рефакторинг legacy кода — это не просто "переписать всё заново", а системная работа по улучшению архитектуры без изменения внешнего поведения.
В этой статье мы разберем пошаговый план рефакторинга на JavaScript (Node.js): от анализа и тестирования до внедрения современных паттернов. Вы узнаете, как безопасно преобразовать "болото" в поддерживаемый код, используя реальные примеры.
1. Анализ и картографирование legacy кода
Прежде чем менять код, нужно понять его текущее состояние. Начните с создания карты зависимостей и выявления "горячих точек".
1.1. Инструменты для анализа
Для JavaScript/Node.js используйте:
- ESLint с правилами
complexityиmax-lines— для выявления сложных функций. - Plato или CodeClimate — для визуализации цикломатической сложности.
- Madge — для построения графа зависимостей модулей.
1.2. Выделение "запахов кода"
Типичные проблемы legacy JS:
- Функции длиной более 100 строк.
- Глобальные переменные и мутация состояния.
- Callback hell (вложенные асинхронные вызовы).
- Отсутствие модульности (один файл на 2000+ строк).
// Пример "запаха": функция делает всё сразу
function processUserData(userId) {
// 100 строк: валидация, запрос к БД, форматирование, отправка email
// ... сложно тестировать и поддерживать
}
2. Подготовка "защитной сети": тестирование
Без тестов рефакторинг — это "русская рулетка". Создайте Characterization Tests (тесты, которые фиксируют текущее поведение).
2.1. Добавление юнит-тестов
Используйте Jest или Mocha. Начните с критических путей:
// Тестируем существующую функцию, не меняя её
const { processUserData } = require('./legacy');
describe('processUserData (legacy)', () => {
it('should process user with ID 1', () => {
const result = processUserData(1);
expect(result).toHaveProperty('email');
expect(result.status).toBe('processed');
});
});
2.2. Интеграционные тесты для Node.js
Если код работает с БД или API, используйте supertest для HTTP-запросов:
const request = require('supertest');
const app = require('../app');
describe('POST /api/users', () => {
it('should create user (legacy behavior)', async () => {
const res = await request(app)
.post('/api/users')
.send({ name: 'Test' });
expect(res.status).toBe(200);
expect(res.body.id).toBeDefined();
});
});
3. Пошаговый рефакторинг: от монолита к модулям
Разделим процесс на 3 этапа: изоляция, декомпозиция, оптимизация.
3.1. Изоляция и обёртки (Wrapper)
Не трогайте внутренности — оберните legacy-функцию в новый интерфейс:
// Legacy функция
function oldFormatDate(date) {
// ... 50 строк с мутациями
return formattedDate;
}
// Новая обёртка
function formatDateSafe(date) {
if (!(date instanceof Date)) {
throw new Error('Invalid date');
}
return oldFormatDate(date);
}
3.2. Декомпозиция с помощью Extract Function
Выделите логические блоки в отдельные функции:
// Было: одна функция на 80 строк
function handleOrder(order) {
// 1. Валидация
if (!order.id || !order.total) { ... }
// 2. Расчёт скидки
let discount = 0;
if (order.total > 100) { discount = 0.1; }
// 3. Сохранение в БД
db.save(order);
// 4. Отправка уведомления
email.send(order);
}
// Стало:
function validateOrder(order) { ... }
function calculateDiscount(total) { ... }
function saveOrder(order) { ... }
function notifyOrder(order) { ... }
function handleOrder(order) {
validateOrder(order);
const discount = calculateDiscount(order.total);
order.discount = discount;
saveOrder(order);
notifyOrder(order);
}
3.3. Замена callback на async/await
Один из главных "бичей" legacy JS — callback hell. Постепенно заменяйте:
// Legacy: callback hell
function getUserData(id, callback) {
db.find(id, (err, user) => {
if (err) return callback(err);
api.fetchProfile(user.profileId, (err, profile) => {
if (err) return callback(err);
callback(null, { user, profile });
});
});
}
// После рефакторинга
async function getUserData(id) {
const user = await db.find(id);
const profile = await api.fetchProfile(user.profileId);
return { user, profile };
}
4. Инструменты и стратегии для Node.js
Рефакторинг на Node.js имеет свою специфику: асинхронность, модульная система CommonJS/ESM, работа с потоками.
4.1. Миграция с CommonJS на ES Modules
Если проект всё ещё на require, начните постепенный переход:
// Было (CommonJS)
const express = require('express');
module.exports = { myFunction };
// Стало (ESM)
import express from 'express';
export const myFunction = () => { ... };
4.2. Внедрение TypeScript (опционально)
TypeScript — мощный инструмент для рефакторинга. Добавьте .ts файлы с минимальными типами:
// legacy.js
function add(a, b) { return a + b; }
// new.ts
function add(a: number, b: number): number {
return a + b;
}