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

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

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

Начать курс

Введение

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

1. Подготовка к рефакторингу: анализ и тестирование

1.1. Инвентаризация кодовой базы

Первый шаг — понять, с чем вы имеете дело. Используйте инструменты статического анализа, такие как Clang-Tidy, PVS-Studio или Cppcheck, чтобы выявить проблемные места: утечки памяти, неопределенное поведение, устаревшие паттерны.

// Пример запуска Clang-Tidy
clang-tidy main.cpp -- -std=c++17 -I./include

Создайте карту зависимостей модулей. Часто legacy-код страдает от циклических зависимостей и "божественных классов".

1.2. Покрытие тестами

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

#include <gtest/gtest.h>

TEST(LegacyModule, ProcessData_ShouldReturnExpected) {
    LegacyClass obj;
    int result = obj.ProcessData(42);
    EXPECT_EQ(result, 84); // Фиксируем текущее поведение
}

Даже если код кажется "неправильным", тест должен отражать его реальную работу. После рефакторинга тесты должны проходить, если вы не меняли логику.

2. Поэтапная модернизация: от макросов к constexpr

2.1. Замена макросов и #define

Макросы — бич legacy-кода. Они не имеют типа, не подчиняются пространствам имен и усложняют отладку. Заменяйте их на constexpr, inline функции или enum class.

// До рефакторинга
#define BUFFER_SIZE 256
#define MAX(a, b) ((a) > (b) ? (a) : (b))

// После рефакторинга
constexpr size_t BUFFER_SIZE = 256;

template<typename T>
constexpr T max(T a, T b) noexcept {
    return (a > b) ? a : b;
}

2.2. Улучшение управления памятью

Ручное управление памятью с new/delete — источник утечек и двойных освобождений. Переходите на умные указатели (std::unique_ptr, std::shared_ptr) и контейнеры STL.

// До рефакторинга
void process() {
    Widget* w = new Widget();
    // ... код ...
    delete w; // легко забыть при исключении
}

// После рефакторинга
void process() {
    auto w = std::make_unique<Widget>();
    // ... код ...
    // автоматическое удаление при выходе из области видимости
}

2.3. Избавление от сырых указателей и массивов

Замените C-стиль массивов на std::array или std::vector, а строки — на std::string.

// До рефакторинга
void copy_data(char* dest, const char* src, size_t len) {
    memcpy(dest, src, len); // риск переполнения буфера
}

// После рефакторинга
void copy_data(std::span<char> dest, std::span<const char> src) {
    std::copy(src.begin(), src.end(), dest.begin());
}

3. Структурные изменения: разбиение и модульность

3.1. Разделение "божественных классов"

Классы, которые делают всё (God Object), — верный признак плохой архитектуры. Используйте принцип единственной ответственности (SRP). Выделите отдельные классы для работы с файлами, логирования, бизнес-логики.

// До: один класс делает всё
class MonsterManager {
    void LoadFromFile(const std::string& path);
    void Render();
    void UpdateAI();
    void SaveToDatabase();
    // ... 50 методов ...
};

// После: разделение на модули
class MonsterLoader { void Load(const std::string& path); };
class MonsterRenderer { void Render(const Monster& m); };
class MonsterAI { void Update(Monster& m); };
class MonsterRepository { void Save(const Monster& m); };

3.2. Внедрение dependency injection

Жесткие зависимости от глобальных переменных или синглтонов делают код нетстируемым. Внедряйте зависимости через конструктор.

// До
class Logger {
public:
    void Log(const std::string& msg) {
        GlobalFile::Write(msg); // жесткая связь
    }
};

// После
class Logger {
    IOutputDevice& device_;
public:
    explicit Logger(IOutputDevice& device) : device_(device) {}
    void Log(const std::string& msg) {
        device_.Write(msg); // легко подменить для тестов
    }
};

4. Повышение безопасности и производительности

4.1. Использование constexpr и compile-time вычислений

Перенесите вычисления, которые не зависят от runtime, на этап компиляции. Это ускоряет программу и делает код самодокументируемым.

constexpr int factorial(int n) {
    return (n <= 1) ? 1 : (n * factorial(n - 1));
}

int main() {
    constexpr int val = factorial(10); // вычислено на этапе компиляции
    std::cout << val;
}

4.2. Применение noexcept и const

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

// До
int GetSize() { return size_; }

// После
int GetSize() const noexcept { return size_; }

4.3. Оптимизация с помощью move-семантики

Вместо к

Блоги

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