Refactoring von Legacy-Code in C++: Eine Schritt-für-Schritt-Anleitung
Die Arbeit mit veraltetem (Legacy-)Code in C++ ist eine der schwierigsten, aber unvermeidlichen Aufgaben in der Karriere eines Entwicklers. Legacy-Code enthält oft keine Modultests, enthält magische Zahlen, Speicherlecks und verworrene Abhängigkeiten. Das Refactoring solchen Codes ohne das Risiko, das System zu beschädigen, ist jedoch möglich, wenn man bewährten Strategien folgt. In diesem Artikel analysieren wir die wichtigsten Ansätze zum Refactoring von Legacy-C++-Code mit praktischen Beispielen.
1. Vorbereitung: Erstellen eines Sicherheitsnetzes
Bevor Sie den Code ändern, müssen Sie ein "Sicherheitsnetz" aus Tests erstellen. Für C++ ist dies besonders wichtig, da Fehler zu undefiniertem Verhalten oder Programmabstürzen führen können.
Schritt 1.1: Charakterisierungstests
Schreiben Sie Tests, die das aktuelle Verhalten des Codes festhalten, selbst wenn es falsch ist. Verwenden Sie Frameworks wie Google Test oder Catch2.
// Beispiel für einen Charakterisierungstest für eine Legacy-Funktion#include <gtest/gtest.h>
// Legacy-Funktion, die wir refactoren werdenint 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}Schritt 1.2: Integration in CI/CD
Fügen Sie die Tests in das Continuous-Integration-System ein. Dies stellt sicher, dass Änderungen die bestehende Logik nicht beschädigen.
2. Strategie: "Intelligentes" Refactoring ohne Schmerz
Versuchen Sie nicht, alles auf einmal umzuschreiben. Verwenden Sie einen inkrementellen Ansatz.
2.1. Extraktion von Schnittstellen (Extract Interface)
Wenn der Code eng mit globalen Zuständen oder bestimmten Klassen verbunden ist, beginnen Sie mit der Extraktion von Abstraktionen.
Problem: Die Klasse arbeitet direkt mit dem Dateisystem.
// Legacy-Codeclass DataProcessor {public: void process(const std::string& filename) { std::ifstream file(filename); // komplexe Verarbeitungslogik }};Lösung: Wir führen eine Schnittstelle und Dependency Injection ein.
// Neue Schnittstelleclass IFileReader {public: virtual std::string read(const std::string& path) = 0; virtual ~IFileReader() = default;};
// Refactoringclass DataProcessor {public: explicit DataProcessor(std::unique_ptr<IFileReader> reader) : reader_(std::move(reader)) {} void process(const std::string& filename) { auto data = reader_->read(filename); // Verarbeitungslogik }
private: std::unique_ptr<IFileReader> reader_;};2.2. Ersetzen magischer Zahlen durch Konstanten
Legacy-Code enthält oft Zahlen, deren Bedeutung unklar ist.
// Vor dem Refactoringif (buffer.size() > 4096) { resizeBuffer(8192);}
// Nach dem Refactoringconstexpr size_t kMaxBufferSize = 4096;constexpr size_t kResizeTarget = 8192;
if (buffer.size() > kMaxBufferSize) { resizeBuffer(kResizeTarget);}3. Bekämpfung von Speicherlecks und unsicheren Konstrukten
C++ Legacy-Code verwendet oft rohe Zeiger und manuelle Speicherverwaltung. Ersetzen Sie diese durch intelligente Zeiger und RAII-Wrapper.
3.1. Ersetzen von new/delete durch std::unique_ptr
// Alter Code (Risiko eines Lecks bei Ausnahme)void processData() { Data* data = new Data(); // ... Arbeit mit data ... delete data;}
// Neuer Code (sicher)void processData() { auto data = std::make_unique<Data>(); // ... Arbeit mit data ... // delete ist nicht nötig}3.2. Verwendung von std::vector anstelle von rohen Arrays
//