Рефакторинг legacy кода на JavaScript: полное руководство

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

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

Начать курс

Введение: Почему 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 — "ад колбэков". Превратите их в последовательные а

Блоги

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