Рефакторинг legacy кода на C++: пошаговое руководство
Работа с устаревшим (legacy) кодом на C++ — одна из самых сложных, но неизбежных задач в карьере разработчика. Legacy-код часто лишен модульных тестов, содержит магические числа, утечки памяти и запутанные зависимости. Однако рефакторинг такого кода без риска сломать систему возможен, если следовать проверенным стратегиям. В этой статье мы разберем ключевые подходы к рефакторингу legacy C++ кода с практическими примерами.
1. Подготовка: создание защитной сети
Прежде чем менять код, необходимо создать «защитную сетку» из тестов. Для C++ это особенно важно, так как ошибки могут привести к неопределенному поведению или падению программы.
Шаг 1.1: Характеризационное тестирование
Напишите тесты, которые фиксируют текущее поведение кода, даже если оно неверно. Используйте фреймворки вроде Google Test или Catch2.
// Пример характеризационного теста для legacy-функции
#include <gtest/gtest.h>
// Legacy-функция, которую мы будем рефакторить
int calculatePrice(int quantity, float basePrice) {
if (quantity > 100) {
return static_cast<int>(basePrice * quantity * 0.9);
} else {
return static_cast<int>(basePrice * quantity);
}
}
TEST(PriceCalculatorTest, LegacyBehavior) {
EXPECT_EQ(calculatePrice(50, 10.0f), 500);
EXPECT_EQ(calculatePrice(150, 10.0f), 1350); // 150*10*0.9
}
Шаг 1.2: Интеграция в CI/CD
Добавьте тесты в систему непрерывной интеграции. Это гарантирует, что изменения не сломают существующую логику.
2. Стратегия: «Умный» рефакторинг без боли
Не пытайтесь переписать всё сразу. Используйте инкрементальный подход.
2.1. Выделение интерфейсов (Extract Interface)
Если код тесно связан с глобальными состояниями или конкретными классами, начните с выделения абстракций.
Проблема: Класс напрямую работает с файловой системой.
// Legacy код
class DataProcessor {
public:
void process(const std::string& filename) {
std::ifstream file(filename);
// сложная логика обработки
}
};
Решение: Вводим интерфейс и инъекцию зависимостей.
// Новый интерфейс
class IFileReader {
public:
virtual std::string read(const std::string& path) = 0;
virtual ~IFileReader() = default;
};
// Рефакторинг
class DataProcessor {
public:
explicit DataProcessor(std::unique_ptr<IFileReader> reader)
: reader_(std::move(reader)) {}
void process(const std::string& filename) {
auto data = reader_->read(filename);
// логика обработки
}
private:
std::unique_ptr<IFileReader> reader_;
};
2.2. Замена магических чисел на константы
Legacy-код часто содержит числа, смысл которых неясен.
// До рефакторинга
if (buffer.size() > 4096) {
resizeBuffer(8192);
}
// После рефакторинга
constexpr size_t kMaxBufferSize = 4096;
constexpr size_t kResizeTarget = 8192;
if (buffer.size() > kMaxBufferSize) {
resizeBuffer(kResizeTarget);
}
3. Борьба с утечками памяти и небезопасными конструкциями
C++ legacy-код часто использует сырые указатели и ручное управление памятью. Замените их на умные указатели и RAII-обертки.
3.1. Замена new/delete на std::unique_ptr
// Старый код (риск утечки при исключении)
void processData() {
Data* data = new Data();
// ... работа с data ...
delete data;
}
// Новый код (безопасный)
void processData() {
auto data = std::make_unique<Data>();
// ... работа с data ...
// delete не нужен
}
3.2. Использование std::vector вместо сырых массивов
// Legacy
int* arr = new int[100];
// ...
delete[] arr;
// Рефакторинг
std::vector<int> arr(100);
4. Инструменты, которые облегчат рефакторинг
Для C++ существует несколько мощных инструментов, помогающих в рефакторинге:
- Clang-Tidy — статический анализатор, который может автоматически исправлять многие проблемы (например, заменять сырые указатели на
std::unique_ptr). - ReSharper C++ (для Visual Studio) — предлагает рефакторинг одним кликом (извлечение метода, переименование, выделение интерфейса).
- Cppcheck — бесплатный анализатор для поиска утечек памяти и других ошибок.
- Include What You Use (IWYU) — оптимизирует заголовочные файлы, что критично для больших проектов.
Пример запуска Clang-Tidy для автоматического исправления:
clang-tidy -fix -checks="modernize-*,cppcoreguidelines-*" main.cpp -- -std=c++17
Заключение
Рефакторинг legacy кода на C++ — это не героическое переписывание, а методичная работа с постоянной проверкой. Главные принципы:
- Не ломай — сначала тесты, потом изменения.
- Двигайся маленькими шагами — один рефакторинг за раз.
- Автоматизируй — используй Clang-Tidy и другие инструменты.
- Внедряй современные идиомы — RAII, умные указатели, constexpr.
Помните: даже небольшое улучшение читаемости или производительности окупается со временем. Начните с малого — например, замените один сырой массив на std::vector — и legacy-код станет чуть более управляемым.
А если вы хотите глубже изучить тему, рекомендуем книгу Майкла Физерса «Эффективная