Введение: Почему legacy код — это не приговор, а задача
Каждый разработчик рано или поздно сталкивается с legacy кодом — устаревшим, плохо документированным и часто не тестированным кодом на JavaScript (Node.js). Работа с таким кодом может вызывать страх и разочарование, но это неизбежная часть профессионального роста. Рефакторинг legacy кода — это не просто переписывание, а системный процесс улучшения внутренней структуры без изменения внешнего поведения.
В этой статье мы разберем пошаговый подход к рефакторингу устаревшего кода на JavaScript, включая стратегии, инструменты и конкретные примеры для Node.js. Вы узнаете, как превратить "кошмарный" код в поддерживаемую, масштабируемую и современную кодовую базу.
1. Анализ и подготовка: с чего начать рефакторинг?
1.1. Аудит существующего кода
Прежде чем менять что-либо, необходимо полностью понять текущую систему. Используйте следующие методы:
- Статический анализ: ESLint с плагинами (eslint-plugin-import, eslint-plugin-security) выявит проблемные паттерны.
- Поиск "красных флагов": функции длиннее 50 строк, глобальные переменные, callback hell, отсутствие модульности.
- Документирование текущего поведения: запишите все входные и выходные данные ключевых функций.
// Пример: поиск "плохих" паттернов с ESLint
// npm install eslint @eslint/js eslint-plugin-security --save-dev
// eslint.config.js
import js from "@eslint/js";
import security from "eslint-plugin-security";
export default [
js.configs.recommended,
security.configs.recommended,
{
rules: {
"no-var": "error",
"prefer-const": "error",
"max-lines-per-function": ["warn", 40],
},
},
];
1.2. Создание страховочной сети: тесты
Правило №1 рефакторинга: никогда не рефакторите код без тестов. Для Node.js используйте Jest или Mocha. Начните с интеграционных тестов, которые проверяют ключевые сценарии работы приложения.
// Пример: тест для legacy функции
// legacy.js
function processOrder(orderData) {
// ... 150 строк запутанного кода
return result;
}
// processOrder.test.js
const processOrder = require('./legacy');
describe('processOrder', () => {
it('должен обработать стандартный заказ', () => {
const input = { id: 1, items: ['book', 'pen'], total: 25 };
const result = processOrder(input);
expect(result.status).toBe('completed');
expect(result.total).toBe(25);
});
it('должен выбросить ошибку при пустом заказе', () => {
expect(() => processOrder({})).toThrow('Invalid order');
});
});
2. Стратегии рефакторинга: от хаоса к порядку
2.1. Стратегия "Strangler Fig" (Удушающий фикус)
Этот подход особенно эффективен для больших Node.js проектов. Вы постепенно заменяете части legacy-системы новыми модулями, не останавливая работу всего приложения.
// Пример: замена старого HTTP-клиента на новый
// Старый код (legacy)
const http = require('http');
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
http.get(`http://api.example.com/users/${userId}`, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve(JSON.parse(data)));
}).on('error', reject);
});
}
// Новый модуль (современный)
const axios = require('axios');
async function fetchUserDataModern(userId) {
const response = await axios.get(`https://api.example.com/users/${userId}`);
return response.data;
}
// Промежуточный слой (фасад)
async function getUserData(userId) {
// Сначала пробуем новый API, при ошибке падаем на старый
try {
return await fetchUserDataModern(userId);
} catch (error) {
console.warn('Falling back to legacy API', error);
return fetchUserData(userId);
}
}
2.2. Выделение Pure Functions
Legacy код часто содержит функции с сайд-эффектами (работа с БД, файловой системой). Выделите чистую логику в отдельные функции — это упростит тестирование.
// До рефакторинга (грязная функция)
function calculateInvoice(userId) {
const user = db.findUser(userId); // сайд-эффект
let total = 0;
for (const item of user.cart) {
total += item.price * item.quantity;
}
total = total * 0.9; // скидка 10%
return total;
}
// После рефакторинга (чистая функция + инъекция зависимостей)
function calculateTotal(items, discount = 0.9) {
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
return subtotal * discount;
}
// Теперь легко тестировать
test('calculateTotal', () => {
const items = [{ price: 100, quantity: 2 }];
expect(calculateTotal(items)).toBe(180);
expect(calculateTotal(items, 1)).toBe(200);
});
3. Практические инструменты и техники для Node.js
3.1. Модуляризация и современный синтаксис
Переход от CommonJS к ES Modules (ESM) и использование async/await — ключевые шаги. Начните с замены require на import и var на const/let.
// Старый стиль (CommonJS + callback)
const fs = require('fs');
const path = require('path');
function readConfig(callback) {
fs.readFile(path.join(__dirname, 'config.json'), 'utf8', (err, data) => {
if (err) return callback(err);
try {
const config = JSON.parse(data);
callback(null, config);
} catch (parseError) {
callback(parseError);
}
});
}
// Новый стиль (ESM + async/await)
import { readFile } from 'fs/promises';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
async function readConfig() {
const data = await readFile(join(__dirname, 'config.json'), 'utf8');
return JSON.parse(data);
}
3.2. Рефакторинг callback hell в цепочки Promise
Одна из самых частых проблем в legacy Node.js — "ад колбэков". Превратите их в последовательные а