Weniger Dogma, mehr Pragmatismus

Clean Code: Die Regeln, die wirklich zählen

· Softwareentwicklung

Clean Code ohne Dogma: Welche Regeln wirklich zählen und wo Pragmatismus wichtiger ist als Lehrbuch-Perfektion.

Clean Code ist ein geflügeltes Wort geworden und wird oft missverstanden, entweder als Religion oder als lästige Pflicht. Hier geht es um eine pragmatische Einordnung.

Das Problem mit dem Dogma

In Code Reviews wird mitunter stundenlang über Methodenlängen diskutiert:

“Diese Methode hat 23 Zeilen. Clean Code sagt maximal 20.”

Um die Zeilenzahl geht es dabei nicht.

Was wirklich zählt

1. Verständliche Namen

Der größte Hebel für lesbaren Code:

# Schlecht
def calc(a, b, c):
    return a * b * (1 - c)

# Gut
def calculate_discounted_price(unit_price, quantity, discount_rate):
    return unit_price * quantity * (1 - discount_rate)

Der zweite Code ist länger und trotzdem besser, weil die Namen ihn dokumentieren.

Als Faustregel: Wenn du einen Kommentar brauchst, um eine Variable zu erklären, ist der Name falsch.

2. Eine Abstraktionsebene pro Funktion

import psycopg2

DSN = "host=db.example.com dbname=shop"


# Vermischt: High-Level und Low-Level
def process_order(order_data):
    # High-Level: Business-Logik
    if order_data["total"] > 1000:
        apply_premium_discount(order_data)

    # Low-Level: Datenbank-Details
    conn = psycopg2.connect(DSN)
    try:
        cursor = conn.cursor()
        cursor.execute("INSERT INTO orders ...")
        conn.commit()
    finally:
        conn.close()

    # High-Level: Benachrichtigung
    notify_warehouse(order_data)


# Besser: Saubere Trennung
def process_order(order_data):
    if is_premium_order(order_data):
        apply_premium_discount(order_data)
    save_order(order_data)
    notify_warehouse(order_data)


def save_order(order_data):
    # Low-Level-Details hier isoliert.
    # "with conn" beendet bei psycopg2 nur die Transaktion,
    # die Verbindung schließt erst conn.close().
    conn = psycopg2.connect(DSN)
    try:
        with conn, conn.cursor() as cursor:
            cursor.execute("INSERT INTO orders ...", order_data)
    finally:
        conn.close()

3. Fail Fast

Probleme früh erkennen, nicht verstecken:

# Schlecht: Fehler versteckt
def get_user_email(user_id):
    try:
        user = db.get_user(user_id)
        return user.email
    except:
        return None  # Was ist passiert? Keine Ahnung.

# Besser: Explizite Fehlerbehandlung
def get_user_email(user_id):
    user = db.get_user(user_id)
    if user is None:
        raise UserNotFoundError(f"User {user_id} not found")
    return user.email

4. Keine Magic Numbers

# Was bedeutet 86400?
cache_timeout = 86400

# Ah, ein Tag in Sekunden
SECONDS_PER_DAY = 86400
cache_timeout = SECONDS_PER_DAY

# Noch besser in Python
from datetime import timedelta
cache_timeout = int(timedelta(days=1).total_seconds())

5. Single Responsibility

Nicht auf Klassen-Ebene verzetteln. Auf Funktions-Ebene anfangen:

# Diese Funktion macht zu viel
def handle_user_registration(form_data):
    # Validierung
    if not form_data.get("email"):
        raise ValidationError("Email required")
    if not is_valid_email(form_data["email"]):
        raise ValidationError("Invalid email")
    
    # Speichern
    user = User(email=form_data["email"])
    db.session.add(user)
    db.session.commit()
    
    # Email senden
    send_welcome_email(user)
    
    # Tracking
    analytics.track("user_registered", user.id)
    
    return user

# Besser: Aufgeteilt
def handle_user_registration(form_data):
    validated_data = validate_registration(form_data)
    user = create_user(validated_data)
    send_welcome_email(user)
    track_registration(user)
    return user

Die unwichtigen Regeln

Worüber sich kein Streit lohnt:

  • Tabs oder Spaces: Das entscheidet Black oder ein anderer Formatter.
  • Zeilenlänge: 80, 100, 120? Egal, Hauptsache konsistent.
  • Methodenlänge: 10 Zeilen oder 30? Kommt auf den Kontext an.
  • Kommentar-Stil: Docstrings oder Inline? Team-Entscheidung.

Diese Dinge löst man einmal im Team, schreibt sie in die .pre-commit-config.yaml und vergisst sie.

DRY: tot oder lebendiger denn je?

Derzeit liest man oft, DRY (Don’t Repeat Yourself) sei obsolet. Die Argumentation: KI generiert Code sowieso, Abstraktionen werden zu komplex, lieber Copy-Paste als überengineerte Generalisierung.

Das stimmt nur zur Hälfte.

Was am DRY-Bashing richtig ist

Übertriebenes DRY führt zu Monstern:

# Das passiert, wenn DRY zum Selbstzweck wird
def process_entity(entity, entity_type, operation, **kwargs):
    handler = get_handler(entity_type, operation)
    validator = get_validator(entity_type)
    transformer = get_transformer(entity_type, operation)
    
    if validator.validate(entity, **kwargs):
        transformed = transformer.transform(entity)
        return handler.execute(transformed, **kwargs)
    ...

Drei Zeilen wurden zu einem Framework, das niemand mehr versteht. Mit DRY hat das wenig zu tun.

Was am DRY-Bashing falsch ist

DRY heißt nicht “keine Wiederholung”. Gemeint ist, dass jede fachliche Wahrheit genau einen Ort im Code hat.

Wenn dieselbe Business-Regel an zwei Stellen steht, wird eine davon irgendwann falsch aktualisiert.

Schon bei zwei Vorkommen kann eine Funktion sinnvoll sein. Die alte Regel “erst ab drei Mal” ignoriert, dass der zweite Ort oft genau der ist, an dem der Bug später entsteht.

Komplexität benennen

Komplexe Ausdrücke gehören in benannte Einzeiler:

import re

# Das hier will niemand lesen
if re.fullmatch(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', email):
    ...

# Das hier schon
def is_valid_email(email: str) -> bool:
    """Prüft, ob email ein gültiges Format hat."""
    pattern = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
    return bool(re.fullmatch(pattern, email))

if is_valid_email(email):
    ...

Der Regex ist immer noch hässlich. Aber jetzt:

  • Hat er einen Namen
  • Ist er dokumentiert
  • Kann ich ihn testen
  • Muss ich ihn nur einmal verstehen

Darum geht es bei DRY: Wissen an einer Stelle zu bündeln, nicht Zeilen zu sparen.

Eine Heuristik

  1. Zweimal gleicher Code: überlegen, ob eine Funktion sinnvoll ist.
  2. Komplexer Ausdruck: immer in eine benannte Funktion, auch wenn er nur einmal vorkommt.
  3. Ähnlicher, aber nicht gleicher Code: in Ruhe lassen, bis das Muster klar wird.
  4. Eine generische Lösung würde zur Parameter-Hölle: lieber zwei spezifische Funktionen.

KI-generierter Code macht DRY nicht obsolet. Im Gegenteil: Wenn KI fleißig kopiert, braucht es Menschen, die aufräumen. GitClear hat 211 Millionen geänderte Codezeilen aus den Jahren 2020 bis 2024 ausgewertet. Im Jahr 2024 hat sich die Häufigkeit duplizierter Codeblöcke ab fünf Zeilen verachtfacht (GitClear: AI Copilot Code Quality, 2025).

Prüffragen im Code Review

  1. Verstehe ich in 30 Sekunden, was die Funktion tut?
  2. Gibt es überraschende Seiteneffekte?
  3. Was passiert im Fehlerfall?
  4. Kann ich das testen?
  5. Würde ich um 3 Uhr nachts diesen Code debuggen wollen?

Pragmatismus vor Perfektion

Clean Code ist kein Selbstzweck. Das Ziel ist:

  • Wartbarkeit: Andere (und du in 6 Monaten) verstehen den Code.
  • Änderbarkeit: Neue Features lassen sich ohne Angst hinzufügen.
  • Testbarkeit: Der Code lässt sich testen.

Wenn ein “unsauberer” Hack diese Ziele erfüllt und die “saubere” Lösung drei Tage dauert, ist der Hack mit einem TODO und einem Ticket die bessere Wahl.

# TODO(UC-1234): Refactor when we support multiple currencies
# Current hack: Hardcoded EUR conversion
price_eur = price_usd * 0.92

Ein so markierter Hack ist besser als eine überengineerte Lösung, die niemand braucht.

Gute Entwickler schreiben Code, den ihre Kollegen verstehen und ändern können, auch wenn er nicht der eleganteste ist.

Clean CodePythonRefactoringCode-QualitätSoftware-Entwicklung