Рефакторинг legacy кода на JavaScript: от А до Я

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

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

Начать курс

Введение: почему 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;
}

4.3. Рефактори

Блоги

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