Refactoring von Legacy-Code in Java: Fortgeschrittenes Niveau
Die Arbeit mit vererbtem (Legacy-)Code ist für jeden professionellen Java-Entwickler eine unvermeidliche Realität. Oft fehlt es solchem Code an Tests, er enthält verworrene Abhängigkeiten, monolithische Klassen und veraltete Muster. Ein einfaches Refactoring „nach Augenmaß“ ist gefährlich – es kann zu Regressionen und zum Stillstand von Geschäftsprozessen führen. In diesem Artikel werden wir fortgeschrittene Techniken zum Refactoring von Legacy-Java-Code besprechen, die Ihnen helfen, das System sicher zu modernisieren, ohne seinen Betrieb zu beeinträchtigen.
1. Die „Drei-Schritte“-Strategie: Schutz vor Änderungen
Das erste und wichtigste Prinzip bei der Arbeit mit Legacy-Code ist die Sicherheit. Beginnen Sie niemals mit dem Refactoring ohne ein schützendes Netz. Verwenden Sie die „Drei-Schritte“-Strategie:
- Verstehen und isolieren – die Grenzen des zu ändernden Moduls identifizieren.
- Mit Tests abdecken (Characterization Tests) – das aktuelle Verhalten festhalten.
- Refactoring – erst danach Änderungen vornehmen.
Verwenden Sie zum Schreiben von Characterization-Tests den Ansatz des „Golden Master Tests“. Führen Sie den Code mit einem Satz von Eingabedaten aus und speichern Sie die Ausgabe. Vergleichen Sie dann nach dem Refactoring die neue Ausgabe mit der Referenz.
// Beispiel: Characterization Test für eine Legacy-Methodepublic class LegacyOrderProcessorTest {
@Test public void testLegacyProcessOrder() { // 1. Eingabedaten festhalten Order order = new Order("LEGACY-001", List.of("item1", "item2")); LegacyOrderProcessor processor = new LegacyOrderProcessor();
// 2. Legacy-Code ausführen String result = processor.processOrder(order);
// 3. Mit vorab gespeicherter Referenz vergleichen (Golden Master) String expected = "Processed: LEGACY-001 - items: [item1, item2]"; assertEquals(expected, result); }}Dieser Test prüft nicht die „Richtigkeit“ der Logik, sondern hält lediglich ihren aktuellen Zustand fest. Nach dem Refactoring muss der Test bestehen bleiben, um die Beibehaltung des Verhaltens zu garantieren.
2. Aufbrechen monolithischer Klassen: Die Technik „Schnittstelle extrahieren“ (Extract Interface)
Große Klassen mit Dutzenden von Methoden und vielen Abhängigkeiten sind ein typisches Merkmal von Legacy-Code. Anstatt zu versuchen, alles auf einmal umzuschreiben, verwenden Sie Extract Interface.
Erstellen Sie ein Interface, das den öffentlichen Vertrag der Klasse beschreibt. Ersetzen Sie dann die direkte Verwendung der Klasse durch das Interface. Dies ermöglicht:
- Abhängigkeiten für Tests zu isolieren.
- Die Implementierung schrittweise in neue, kleinere Klassen auszulagern.
- Mock-Objekte einzuführen, ohne den Client-Code zu ändern.
// Ursprüngliche Legacy-Klassepublic class HugeDataManager { public void saveData(String data) { /* ... komplexe Logik */ } public String loadData(String key) { /* ... */ } public void deleteAll() { /* ... gefährliche Operation */ } // ... weitere 50 Methoden}
// Schritt 1: Schnittstelle extrahierenpublic interface DataRepository { void saveData(String data); String loadData(String key);}
// Schritt 2: Legacy-Klasse implementiert das Interfacepublic class HugeDataManager implements DataRepository { @Override public void saveData(String data) { /* ... */ } @Override public String loadData(String key) { /* ... */ } // deleteAll() bleibt nur in der Klasse, nicht im Interface}
// Schritt 3: Client-Code arbeitet über das Interfacepublic class DataService { private final DataRepository repository;
public DataService(DataRepository repository) { this.repository = repository; }}Jetzt können Sie einen Test für DataService schreiben, indem Sie einen Mock für DataRepository verwenden, ohne den komplexen HugeDataManager anzufassen.
3. Arbeiten mit veralteten Collections und Schleifen
Legacy-Java-Code (Java 7