Рефакторинг legacy кода на JavaScript: продвинутый уровень
Работа с унаследованным (legacy) кодом — неизбежная реальность для любого разработчика, особенно в сфере Node.js. Часто такой код представляет собой «болото» из глобальных переменных, вложенных callback'ов, функций длиной в сотни строк и полного отсутствия модульной структуры. Простые методы рефакторинга здесь не работают: требуется системный подход и знание продвинутых техник. В этой статье мы разберем стратегии, которые помогут превратить хаос в поддерживаемую архитектуру без остановки продакшена.
1. Анализ и картографирование зависимостей
Прежде чем менять код, его нужно понять. В legacy-проектах документация часто отсутствует или устарела. Первый шаг — построить карту зависимостей.
Инструменты:
- Madge — утилита для визуализации зависимостей между модулями. Она покажет циклические зависимости и «мертвые» файлы.
- ESLint с плагинами — поможет выявить неиспользуемые переменные, функции и импорты.
- Деплои в изолированную среду — запустите тесты (если их нет, напишите хотя бы smoke-тесты) и запишите, какие модули загружаются.
Пример: Допустим, у нас есть файл legacy.js, который содержит 2000 строк и использует глобальную переменную config.
// legacy.js (до рефакторинга)
var config = {
apiUrl: 'https://api.example.com',
timeout: 5000
};
function fetchData(callback) {
// 50 строк логики с вложенными if и try/catch
// использует глобальный config
if (config.timeout > 0) {
// ...
}
callback(null, data);
}
Действие: С помощью Madge мы видим, что legacy.js импортируется в 15 других файлах. Это «божественный объект». Наша задача — разбить его на атомарные модули.
2. Стратегия «Strangler Fig» (Фиговый душитель)
Этот паттерн идеален для рефакторинга без остановки системы. Суть: мы не переписываем всё сразу, а постепенно заменяем части старой системы новыми, пока старая не умрёт сама.
Реализация в Node.js:
- Создаём новый модуль с чистой архитектурой.
- Пишем прокси-слой (или фасад), который перенаправляет вызовы к старому или новому модулю.
- Постепенно переключаем потребителей на новый модуль.
Пример: Заменяем старый callback-стиль на Promise/async/await.
// Старый API (callback-based)
function getUser(id, callback) {
// legacy код
callback(null, { id: id, name: 'John' });
}
// Новый API (Promise-based)
async function getUserAsync(id) {
// новая реализация с async/await
const user = await database.findUser(id);
return user;
}
// Прокси-фасад
async function getUserFacade(id) {
if (USE_NEW_IMPLEMENTATION) {
return getUserAsync(id);
} else {
return new Promise((resolve, reject) => {
getUser(id, (err, user) => {
if (err) reject(err);
else resolve(user);
});
});
}
}
Флаг USE_NEW_IMPLEMENTATION можно вынести в конфигурацию или feature toggle. Это позволяет включать новый код для части пользователей (canary release) или откатывать изменения без деплоя.
3. Модуляризация и изоляция побочных эффектов
Legacy-код часто страдает от скрытых побочных эффектов: функция меняет глобальное состояние, пишет в файл или вызывает внешний API, не возвращая результат. Рефакторинг таких функций — это выделение чистой логики.
Техника: «Извлечение функции» (Extract Function) с последующим внедрением зависимостей (Dependency Injection).
Проблемный код:
// Старый код: функция делает всё
function processOrder(orderId) {
const order = db.query(`SELECT * FROM orders WHERE id = ${orderId}`); // побочный эффект
const total = order.items.reduce((sum, item) => sum + item.price, 0);
fs.writeFileSync('/tmp/log.txt', `Order ${orderId} processed`); // ещё один побочный эффект
return total;
}
Рефакторинг: Разделяем на три модуля: работа с БД, расчёт суммы, логирование.
// orderRepository.js
async function getOrderById(orderId) {
// чистая абстракция доступа к данным
return await db.query('SELECT * FROM orders WHERE id = $1', [orderId]);
}
// orderCalculator.js
function calculateTotal(order) {
// чистая функция, без побочных эффектов
return order.items.reduce((sum, item) => sum + item.price, 0);
}
// logger.js
function logProcessing(orderId) {
// изолированный побочный эффект
fs.appendFileSync('/var/log/orders.log', `Order ${orderId} processed at ${Date.now()}\n`);
}
// Основной модуль — оркестратор
async function processOrder(orderId) {
const order = await getOrderById(orderId);
const total = calculateTotal(order);
logProcessing(orderId);
return total;
}
Теперь каждый модуль можно тестировать изолированно. calculateTotal — чистая функция, её легко покрыть unit-тестами. getOrderById и logProcessing можно замокать.
4. Миграция на TypeScript и строгую типизацию
Один из самых мощных шагов для улучшения legacy-кода — добавление типов. TypeScript не только ловит ошибки, но и служит живой документацией. Однако в большом проекте внедрять его нужно постепенно.
Стратегия:
- allowJs: true — TypeScript может работать с .js файлами. Начните с добавления
tsconfig.json. - checkJs: true — включите проверку типов для .js файлов. Сначала будет много ошибок, но это покажет проблемные места.
- Добавление JSDoc аннотаций — вместо полного переписывания файлов, добавьте типы через JSDoc. Это даст автодополнение и проверку без компиляции.
-
Блоги
Рекомендаций по книгам
Читайте также