Рефакторинг legacy кода на C++: продвинутые методы и стратегии

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

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

Начать курс

Введение: Почему legacy-код на C++ — это вызов для профессионала

Работа с унаследованным кодом на C++ — это не просто задача, а искусство. Legacy-проекты, написанные 10–15 лет назад, часто содержат сырые указатели, длинные функции, макросы препроцессора и отсутствие модульных тестов. Однако просто переписать всё с нуля — роскошь, которую редко может позволить бизнес. Продвинутый рефакторинг позволяет постепенно модернизировать код, снижая технический долг и повышая производительность без остановки разработки.

В этой статье мы рассмотрим продвинутые техники рефакторинга для опытных C++ разработчиков: от избавления от макросов до внедрения RAII и умных указателей, а также стратегии безопасного изменения архитектуры.

1. Анализ и картографирование зависимостей

Перед любым рефакторингом необходимо понять, как устроен текущий код. Для этого используйте статический анализ (например, Clang Static Analyzer или Cppcheck) и инструменты визуализации зависимостей (Doxygen + Graphviz).

1.1. Построение графа вызовов

Создайте граф вызовов для ключевых модулей. Это поможет выявить:

  • Циклические зависимости между модулями;
  • «Божественные объекты» (God classes), которые знают слишком много;
  • Мёртвый код, который никогда не вызывается.
// Пример: использование Doxygen для генерации графа
// В комментариях к функциям указываем зависимости
/**
 * @brief Обрабатывает запрос пользователя
 * @param request - структура запроса
 * @return true, если обработка успешна
 * @see UserManager, RequestValidator
 */
bool ProcessRequest(const Request& request);

1.2. Создание тестовой обвязки (Test Harness)

Без тестов рефакторинг — это хождение по минному полю. Начните с написания характеризационных тестов (characterization tests), которые фиксируют текущее поведение кода. Используйте Google Test или Catch2 для C++.

// Пример характеризационного теста для старой функции
TEST(LegacyModuleTest, CalculateDiscount_ReturnsCorrectValue) {
    // Arrange
    LegacyCalculator calc;
    // Act
    double result = calc.CalculateDiscount(100.0, "VIP");
    // Assert — фиксируем текущее поведение, даже если оно кажется нелогичным
    ASSERT_DOUBLE_EQ(result, 85.0);
}

2. Замена макросов на constexpr и inline-функции

Макросы (#define) — главный враг читаемости и безопасности типов в legacy C++. Они не подчиняются правилам области видимости и могут вызывать трудноуловимые баги.

2.1. Пошаговая замена

Вместо того чтобы удалять все макросы сразу, используйте стратегию «Strangler Fig»:

  1. Найдите макрос и определите все места его использования.
  2. Замените его на constexpr константу или inline функцию.
  3. Перекомпилируйте и прогоните тесты.
  4. Удалите оригинальный #define.
// Было (legacy):
#define MAX_BUFFER_SIZE 4096
#define SQUARE(x) ((x) * (x))

// Стало (modern C++):
constexpr size_t kMaxBufferSize = 4096;
template<typename T>
constexpr T Square(T x) noexcept {
    return x * x;
}

Важно: Для макросов, которые ведут себя как функции (с аргументами), используйте variadic templates или std::invoke, если макрос использовал __VA_ARGS__.

3. Переход от сырых указателей к умным

Сырые указатели с ручным управлением памятью (new/delete) — источник утечек и двойных удалений. Продвинутый рефакторинг предполагает внедрение RAII и умных указателей.

3.1. Анализ владения

Прежде чем менять указатели, определите модель владения:

  • Исключительное владение — используйте std::unique_ptr.
  • Разделяемое владение — используйте std::shared_ptr (но с осторожностью, чтобы избежать циклических ссылок).
  • Наблюдение без владения — используйте сырой указатель или std::observer_ptr (из GSL).
// Было (legacy):
class Image {
    Pixel* data_;
    size_t size_;
public:
    Image(size_t s) : size_(s), data_(new Pixel[s]) {}
    ~Image() { delete[] data_; }
    // Проблема: нарушено правило трёх (копирование/присваивание)
};

// Стало (modern C++):
class Image {
    std::vector<Pixel> data_; // RAII-контейнер
public:
    explicit Image(size_t s) : data_(s) {}
    // Деструктор, копирование и присваивание — автоматически корректны
};

3.2. Работа с устаревшими API

Если legacy-код требует сырых указателей для взаимодействия с C-библиотеками, используйте паттерн «адаптер»:

// Адаптер для C-функции, которая принимает сырой указатель
class LegacyApiWrapper {
    std::unique_ptr<InternalData, decltype(&DestroyInternal)> handle_;
public:
    LegacyApiWrapper() 
        : handle_(CreateInternal(), &DestroyInternal) {}
    
    // Предоставляем доступ к сырому указателю только при необходимости
    InternalData* GetRawHandle() const noexcept {
        return handle_.get();
    }
};

4. Рефакторинг больших функций и циклических зависимостей

Функции длиной более 100 строк — типичный признак legacy-кода. Разбивайте их на мелкие, тестируемые единицы.

4.1. Выделение чистых функций

Ищите блоки кода, которые можно выделить в отдельные функции без побочных эффектов. Используйте const и noexcept везде, где это возможно.

// Было: одна большая функция на 200 строк
void ProcessOrder(Order& order) {
    // 50                

Блоги

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