Введение: Почему 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»:
- Найдите макрос и определите все места его использования.
- Замените его на
constexprконстанту илиinlineфункцию. - Перекомпилируйте и прогоните тесты.
- Удалите оригинальный
#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