Legacy Code modernisieren: Ein Survival Guide
Strategien für den Umgang mit gewachsenen Codebases
Nach 35 Jahren in der IT habe ich mehr Legacy-Code gesehen als mir lieb ist. Hier sind meine Strategien, um damit umzugehen – ohne alles neu zu schreiben.
Was ist eigentlich Legacy Code?
Michael Feathers hat es treffend formuliert: Legacy Code ist Code ohne Tests.
Aber in der Praxis ist es mehr:
- Code, den keiner mehr versteht
- Code, den keiner anfassen will
- Code, dessen ursprüngliche Entwickler längst weg sind
- Code, der “einfach funktioniert” (meistens)
Die Versuchung des Rewrites
“Lass uns das neu schreiben. In React. Mit Microservices. In der Cloud.”
Ich habe diesen Satz hundertfach gehört. Und hundertfach gesehen, wie Rewrites scheitern:
- Das alte System läuft weiter (muss ja)
- Das neue System dauert länger als geplant (immer)
- Features werden doppelt entwickelt (frustrierend)
- Am Ende: zwei halbfertige Systeme
Meine Faustregel: Ein Rewrite ist nur gerechtfertigt, wenn das Geschäftsmodell sich fundamental ändert. Sonst: Modernisieren.
Strategie 1: Die Strangler Fig Pattern
Benannt nach Würgefeigen, die ihren Wirtsbaum langsam ersetzen.
[Alter Monolith]
|
[API Gateway]
|
/ | \
[Alt] [Neu] [Alt]
- Proxy davor – Alle Requests gehen durch eine neue Schicht
- Feature für Feature – Neue Implementierung ersetzt alte
- Transparenter Übergang – User merken nichts
- Alte Komponenten sterben – Wenn nichts mehr durchgeht
Beispiel
Ein Perl-basiertes Bestellsystem. Statt Rewrite:
# Neuer FastAPI-Service
@app.post("/orders")
async def create_order(order: OrderRequest):
if feature_flag("new_order_processing"):
return await new_order_service.create(order)
else:
return await legacy_perl_proxy.create(order)
Feature Flags ermöglichen graduellen Rollout.
Strategie 2: Charakterisierungstests
Bevor man Code ändert, muss man sein Verhalten verstehen.
def test_characterization_order_total():
"""
Charakterisierungstest: Dokumentiert aktuelles Verhalten.
ACHTUNG: Dieser Test dokumentiert, was der Code TUT,
nicht was er tun SOLLTE. Änderungen nur nach Review.
"""
order = create_legacy_order(
items=[("SKU123", 2), ("SKU456", 1)],
discount_code="SUMMER10"
)
# Das ist das aktuelle Verhalten
assert order.total == 47.23 # Nicht 47.20!
assert order.tax == 7.23
# Bug oder Feature? Wir wissen es nicht.
# Aber jetzt haben wir einen Test.
Diese Tests sind Gold wert. Sie:
- Dokumentieren das tatsächliche Verhalten
- Schützen vor unbeabsichtigten Änderungen
- Ermöglichen sicheres Refactoring
Strategie 3: Die Bubble-Technik
Eine saubere Zone im Legacy-Code schaffen.
# legacy_integration.py
# ========================================
# ACHTUNG: Legacy-Grenze
# Alles unterhalb ist alter Code.
# Nicht direkt aufrufen!
# ========================================
from legacy import horrible_function
def get_user_orders(user_id: int) -> list[Order]:
"""
Saubere Schnittstelle zum Legacy-System.
Übersetzt zwischen moderner API und Legacy-Datenstrukturen.
"""
legacy_result = horrible_function(user_id, None, "ORDERS", 1)
return [_convert_legacy_order(o) for o in legacy_result["data"]]
def _convert_legacy_order(legacy_order: dict) -> Order:
"""Konvertiert Legacy-Datenstruktur in modernes Model."""
return Order(
id=legacy_order["ORDER_ID"],
total=Decimal(str(legacy_order["TOTAL_AMOUNT"])),
# ... weitere Mappings
)
Neuer Code ruft nur die saubere Schnittstelle auf. Legacy-Details bleiben isoliert.
Strategie 4: Die Boy Scout Rule
“Hinterlasse den Code besser, als du ihn vorgefunden hast.”
Kein großes Refactoring, aber stetige kleine Verbesserungen:
# Vorher
def calc(a,b,c,d,e):
x=a+b
if c>0:
x=x*c
return x+d-e
# Nachher (nach einem Bug-Fix in der Nähe)
def calculate_adjusted_total(
base_amount: float,
tax: float,
multiplier: float,
bonus: float,
discount: float
) -> float:
"""
Berechnet den angepassten Gesamtbetrag.
Note: Multiplier wird nur angewendet wenn > 0.
Legacy-Verhalten, Grund unbekannt.
"""
subtotal = base_amount + tax
if multiplier > 0:
subtotal *= multiplier
return subtotal + bonus - discount
Jede Änderung macht den Code ein bisschen besser.
Strategie 5: Dokumentation durch Tests
Tests als lebende Dokumentation:
class TestLegacyOrderCalculation:
"""
Dokumentiert das Verhalten des Legacy-Bestellsystems.
Diese Tests wurden durch Reverse Engineering erstellt.
Sie dokumentieren Verhalten, nicht Spezifikation.
"""
def test_discount_applied_before_tax(self):
"""Rabatte werden VOR der Steuer abgezogen."""
order = create_order(subtotal=100, discount=10, tax_rate=0.19)
# Erwartet: (100 - 10) * 1.19 = 107.10
assert order.total == Decimal("107.10")
def test_minimum_order_value_ignored_for_premium(self):
"""
Premium-Kunden haben keinen Mindestbestellwert.
Bug oder Feature? Entscheidung von 2008, niemand weiß warum.
"""
order = create_order(subtotal=5, customer_type="premium")
assert order.is_valid is True
Warnsignale
Wann Modernisierung kritisch wird:
- Keine Tests – Jede Änderung ist Russisch Roulette
- Kein Versionskontrolle – Ja, das gibt es noch
- Ein Entwickler – Wenn der geht, stirbt das Wissen
- “Das darf man nicht anfassen” – Code-Tabus sind gefährlich
- Dokumentation: “Der Code ist die Doku” – Klassiker
Das dauert länger als ein Rewrite? Ja. Aber es funktioniert.