Home

02 Code-Qualität in der Praxis: Refactoring - Vom Monolithen zu Modulen

lecture refactoring modularity code-organization separation-of-concerns software-architecture

1. Einführung: Ihre CI ist grün, aber ist Ihr Code bereit zu wachsen?

Herzlichen Glückwunsch! Sie haben den ersten großen Meilenstein der professionellen Entwicklung erreicht:

Ihre CI-Pipeline ist grün. Ihr Code ist richtig formatiert. Ihre Commits sind sauber.

Aber hier ist die unbequeme Wahrheit:

Ihre Road Profile Viewer-Anwendung lebt in einer einzigen 390-Zeilen-Datei main.py. Und obwohl diese Datei alle Qualitätschecks besteht, wird sie zu einem Problem.

1.1 Der Schmerz, den Sie gleich spüren werden

Stellen Sie sich diese Szenarien vor (Sie werden sie wahrscheinlich bald erleben):

Szenario 1: Die Bug-Jagd

Sie: "Da ist ein Bug in der Schnittpunkt-Berechnung."
Auch Sie: *Scrollt durch 390 Zeilen auf der Suche nach find_intersection()*
Auch Sie: *Passiert generate_road_profile(), create_dash_app(), update_graph()...*
Auch Sie: "Gefunden! Zeile 267. Moment, hängt diese Funktion von etwas anderem ab?"

Szenario 2: Der Merge-Konflikt

Student A: Arbeitet an Verbesserung des Straßengenerierungs-Algorithmus (Zeilen 50-120)
Student B: Arbeitet an UI-Styling-Verbesserungen (Zeilen 200-350)
Student A: Pusht Änderungen
Student B: Versucht zu pushen → MERGE-KONFLIKT
Beide Studenten: Verbringen 30 Minuten mit dem Lösen von Konflikten in derselben Datei

Szenario 3: Der Test-Albtraum (Vorschau auf Kapitel 03 (Grundlagen des Testens))

Sie: "Ich möchte testen, ob find_intersection() leere Arrays korrekt behandelt."
Sie: *Schreibt Test*
Test: *Startet die gesamte Dash-Anwendung nur um eine Funktion zu testen*
Sie: "Das dauert 5 Sekunden. Ich habe 20 Funktionen zu testen..."
Sie: *Gibt das Testen auf*

Das Kernproblem: Ihr ganzer Code ist miteinander verwoben. Mathe, UI, Datengenerierung - alles an einem Ort.


2. Lernziele

Am Ende dieser Vorlesung werden Sie:

  1. Verstehen, warum monolithischer Code nicht wartbar wird, wenn Projekte wachsen
  2. Lernen, natürliche Grenzen zu identifizieren in Ihrem Code (wo man trennen sollte)
  3. Eine monolithische Datei refactoren in fokussierte Module mit einem einzelnen Zweck
  4. Imports und Abhängigkeiten organisieren, um zirkuläre Abhängigkeiten zu vermeiden
  5. Die Vorteile von modularem Code erleben (einfachere Navigation, Tests, Zusammenarbeit)
  6. Ihre Codebasis für Tests vorbereiten (Kapitel 03 (Grundlagen des Testens)) durch Trennung von Zuständigkeiten

Was Sie NICHT (noch) lernen werden:


3. Anwendung des professionellen Feature-Development-Workflows

Bevor wir ins Refactoring eintauchen, lassen Sie uns festlegen, wie wir diese Arbeit angehen werden. In Kapitel 02 (Feature-Entwicklung) haben Sie den professionellen Feature-Development-Workflow gelernt:

  1. Erstellen Sie einen Feature-Branch
  2. Implementieren Sie Ihre Änderungen
  3. Pushen Sie zu GitHub
  4. Öffnen Sie einen Pull Request
  5. Automatisierte CI-Checks laufen
  6. Code Review
  7. Merge wenn genehmigt
  8. Löschen Sie den Branch

Das heutige Refactoring ist ein Feature! Wir werden genau diesen Workflow anwenden.

3.1 Schritt 1: Erstellen Sie Ihren Feature-Branch

Beginnen Sie damit, einen Feature-Branch für diese Refactoring-Arbeit zu erstellen:

# Stellen Sie sicher, dass Sie auf main sind und aktuell
$ git checkout main
$ git pull origin main

# Erstellen Sie einen neuen Feature-Branch
$ git checkout -b feature/refactor-to-modules
Switched to a new branch 'feature/refactor-to-modules'

# Überprüfen Sie, dass Sie auf dem neuen Branch sind
$ git branch
  main
* feature/refactor-to-modules

Warum wir das machen:

Von diesem Punkt an findet all Ihre Refactoring-Arbeit auf diesem Branch statt.

3.2 Der Plan: Iteratives Refactoring mit Commits

Wir werden in kleinen, sicheren Schritten refactoren mit einem Commit nach jedem Schritt:

Schritt 1: Extrahiere geometry.py → Commit → Test
Schritt 2: Extrahiere road.py → Commit → Test
Schritt 3: Extrahiere visualization.py → Commit → Test
Schritt 4: Vereinfache main.py → Commit → Test
Final: Pushen Sie Branch → Öffnen Sie PR → CI validiert → Merge

Schlüsselprinzip: Nach jedem Schritt sollte die Anwendung noch perfekt funktionieren.

Warum kleine Schritte wichtig sind:


4. Das Monolith-Problem verstehen

4.1 Was ist ein Monolith?

Ein Monolith (in Software) ist Code, wo alles an einem Ort lebt, eng miteinander gekoppelt.

Ihr aktuelles main.py:

"""
Road Profile Viewer - Interactive 2D Visualization
===================================================
This module contains the entire application in a single file...
"""

import numpy as np
from dash import Dash, html, dcc, Input, Output
import plotly.graph_objects as go

# Road profile generation (lines 20-60)
def generate_road_profile(num_points=100, x_max=80):
    # ... 40 lines of numpy calculations ...
    return x, y

# Geometry calculations (lines 60-180)
def calculate_ray_line(angle_degrees, camera_x=0, camera_y=2.0, x_max=80):
    # ... calculating ray intersections ...

def find_intersection(x_road, y_road, angle_degrees, camera_x=0, camera_y=1.5):
    # ... finding where ray hits road ...
    return x_intersect, y_intersect, distance

# Dash application (lines 190-390)
def create_dash_app():
    app = Dash(__name__)
    app.layout = html.Div([...])  # UI definition

    @app.callback(...)
    def update_graph(angle):
        # ... mixing UI logic with calculations ...

    return app

# Entry point
def main():
    app = create_dash_app()
    app.run(debug=True)

if __name__ == '__main__':
    main()

Alles ist durcheinander:

4.2 Warum Monolithen zu Problemen werden

1. Schwer zu Navigieren

Spezifische Funktionalität zu finden erfordert durch unabhängigen Code zu scrollen.

# Sie wollen Schnittpunkt-Logik ändern
# Aber zuerst müssen Sie:
# - Imports überspringen (20 Zeilen)
# - Straßengenerierung überspringen (40 Zeilen)
# - Hilfsfunktionen überspringen (60 Zeilen)
# - Endlich find_intersection() bei Zeile 111 erreichen

2. Schwer zu Testen (Sie werden diesen Schmerz in Kapitel 03 (Grundlagen des Testens) spüren)

# Um find_intersection() zu testen, brauchen Sie:
# - Alles importieren (einschließlich Dash, was einen Webserver startet)
# - Auf langsame Imports warten
# - Riskiere Seiteneffekte von anderem Code

# Was Sie wollen:
from geometry import find_intersection  # Schnell, fokussierter Import

3. Schwer Zusammenzuarbeiten

# Zwei Entwickler arbeiten an derselben Datei:
Entwickler A: Ändert Zeile 50 (Straßengenerierung)
Entwickler B: Ändert Zeile 300 (UI-Styling)
Git: MERGE-KONFLIKT (obwohl die Änderungen unabhängig sind!)

4. Hohe Kopplung

Wenn alles in einer Datei ist, ist es einfach unsichtbare Abhängigkeiten zu erstellen:

def find_intersection(...):
    # Verwendet versehentlich eine globale Variable aus dem UI-Bereich
    camera_x = CAMERA_POSITION  # Wo ist das definiert?
    # Jetzt hängt Geometrie von UI ab - schwer isoliert zu testen!

5. Verletzt das “Eine Datei, Ein Zweck”-Prinzip

Fragen Sie sich selbst: “Was macht main.py?”

Aktuelle Antwort: “Alles! Mathe, UI, Datengenerierung, App-Start…”

Bessere Antwort (nach Refactoring): “Startet die Anwendung. Das war’s.”


4. Teil 2: Natürliche Grenzen identifizieren

Bevor wir mit dem Refactoring beginnen, müssen wir verstehen, wo wir trennen sollen.

4.1 Der “Kann ich es in 3 Wörtern beschreiben?”-Test

Wenn Sie den Zweck eines Moduls in 3 Wörtern oder weniger beschreiben können, ist es fokussiert genug.

Gute Beispiele:

Schlechte Beispiele:

4.2 Ihren aktuellen Code analysieren

Lassen Sie uns jede Funktion in main.py kategorisieren nach wovon sie abhängt:

4.2.1 Kategorie 1: Reine Mathe-Funktionen (Abhängig von Nichts)

def calculate_ray_line(angle_degrees, camera_x=0, camera_y=2.0, x_max=80):
    """
    Calculate the line representing the camera ray.

    Depends on: Only numpy and the input parameters
    Side effects: None
    Returns: Calculated coordinates
    """
    angle_rad = -np.deg2rad(angle_degrees)
    slope = np.tan(angle_rad)
    # ... pure calculations ...
    return x_ray, y_ray

def find_intersection(x_road, y_road, angle_degrees, camera_x=0, camera_y=1.5):
    """
    Find intersection between camera ray and road profile.

    Depends on: Only numpy and the input parameters
    Side effects: None
    Returns: Calculated coordinates and distance
    """
    angle_rad = -np.deg2rad(angle_degrees)
    # ... pure calculations ...
    return x_intersect, y_intersect, distance

Schlüssel-Charakteristik: Diese Funktionen kümmern sich nicht um Dash, UI oder irgendetwas anderes. Sie sind rein: dieselben Inputs produzieren immer dieselben Outputs.

Wo sie hingehören: geometry.py (fokussiert auf geometrische Berechnungen)

4.2.2 Kategorie 2: Datengenerierung (Abhängig von Mathe, nicht UI)

def generate_road_profile(num_points=100, x_max=80):
    """
    Generate a road profile using a clothoid-like approximation.

    Depends on: Only numpy
    Side effects: None (doesn't draw anything or start servers)
    Returns: Road coordinates
    """
    x = np.linspace(0, x_max, num_points)
    x_norm = x / x_max
    y = 0.015 * x_norm**3 * x_max + ...
    return x, y

Schlüssel-Charakteristik: Generiert Daten, zeigt sie aber nicht an.

Wo sie hingehört: road.py (fokussiert auf Straßengenerierungs-Logik)

4.2.3 Kategorie 3: UI und Anwendungslogik (Abhängig von Allem)

def create_dash_app():
    """
    Create and configure the Dash application.

    Depends on: Dash, plotly, geometry functions, road functions
    Side effects: Creates web server
    Returns: Dash app instance
    """
    app = Dash(__name__)

    app.layout = html.Div([
        html.H1("Road Profile Viewer..."),
        dcc.Input(id='angle-input', ...),
        dcc.Graph(id='road-profile-graph'),
    ])

    @app.callback(
        [Output('road-profile-graph', 'figure'), ...],
        [Input('angle-input', 'value')]
    )
    def update_graph(angle):
        # Uses road generation
        x_road, y_road = generate_road_profile(...)

        # Uses geometry
        x_intersect, y_intersect, distance = find_intersection(...)

        # Creates visualization
        fig = go.Figure()
        fig.add_trace(...)
        return fig, info_text

    return app

Schlüssel-Charakteristik: Orchestriert alles, erstellt UI, behandelt Benutzereingaben.

Wo sie hingehört: visualization.py (fokussiert auf Präsentationsschicht)

4.2.4 Kategorie 4: Einstiegspunkt (Abhängig von Visualization)

def main():
    """
    Main function to run the Dash application.

    Depends on: create_dash_app()
    Side effects: Starts web server
    """
    app = create_dash_app()
    print("Starting Road Profile Viewer...")
    print("Open your browser and navigate to: http://127.0.0.1:8050/")
    app.run(debug=True)

if __name__ == '__main__':
    main()

Schlüssel-Charakteristik: Startet nur die Anwendung. Minimale Logik.

Wo sie hingehört: main.py (der Einstiegspunkt, so einfach wie möglich)

4.3 Die Abhängigkeits-Hierarchie

Beachten Sie den natürlichen Abhängigkeitsfluss:

main.py
    ↓ abhängig von
visualization.py
    ↓ abhängig von
geometry.py, road.py
    ↓ abhängig von
numpy (externe Bibliothek)

Schlüssel-Einsicht: Abhängigkeiten sollten nur in eine Richtung fließen.

Warum? Das verhindert zirkuläre Abhängigkeiten (Modul A importiert Modul B importiert Modul A…).


5. Teil 3: Der Refactoring-Plan

5.1 Zielstruktur

road-profile-viewer/
├── main.py                 # Nur Einstiegspunkt (~15 Zeilen)
├── geometry.py            # Reine Mathe-Funktionen (~80 Zeilen)
├── road.py                # Straßengenerierung (~60 Zeilen)
├── visualization.py       # Dash-App und UI (~200 Zeilen)
└── (andere Dateien bleiben gleich)

5.2 Schritt-für-Schritt Refactoring-Anleitung

Wir werden in kleinen, sicheren Schritten refactoren. Nach jedem Schritt werden wir überprüfen, dass die Anwendung noch funktioniert.


6. Teil 4: Praktisches Refactoring (Mach mit!)

6.1 Schritt 1: Erstellen Sie die neuen Modul-Dateien

Zuerst lassen Sie uns leere Dateien für unsere neuen Module erstellen:

# In Ihrem road-profile-viewer-Verzeichnis
$ touch geometry.py
$ touch road.py
$ touch visualization.py

Überprüfen Sie die Struktur:

$ ls *.py
geometry.py  main.py  road.py  visualization.py

6.2 Schritt 2: Extrahieren Sie reine Geometrie-Funktionen → geometry.py

Ziel: Verschieben Sie alle reinen Mathe-Funktionen, die nicht von Dash oder Straßengenerierung abhängen.

Öffnen Sie geometry.py und fügen Sie hinzu:

"""
Geometry calculations for ray-road intersection.

This module contains pure mathematical functions for calculating:
- Camera ray lines at various angles
- Intersection points between rays and road profiles
- Distances from camera to intersection points

All functions are pure (no side effects) and depend only on numpy.
"""

import numpy as np


def calculate_ray_line(
    angle_degrees: float,
    camera_x: float = 0,
    camera_y: float = 2.0,
    x_max: float = 80
) -> tuple[np.ndarray, np.ndarray]:
    """
    Calculate the line representing the camera ray.

    Parameters
    ----------
    angle_degrees : float
        Angle in degrees from the positive x-axis (measured downward from horizontal)
    camera_x : float, optional
        X-coordinate of camera position (default: 0)
    camera_y : float, optional
        Y-coordinate of camera position (default: 2.0)
    x_max : float, optional
        Maximum x extent for the ray (default: 80)

    Returns
    -------
    tuple[np.ndarray, np.ndarray]
        x and y coordinates of the ray line

    Examples
    --------
    >>> x_ray, y_ray = calculate_ray_line(-10.0)
    >>> len(x_ray)
    2
    """
    # Convert angle to radians (angle is measured downward from horizontal)
    # Negative angle because y-axis points up but we measure downward angle
    angle_rad = -np.deg2rad(angle_degrees)

    # Calculate slope
    if np.abs(np.cos(angle_rad)) < 1e-10:
        # Vertical line case
        return np.array([camera_x, camera_x]), np.array([camera_y, -10])

    slope = np.tan(angle_rad)

    # Calculate x range where the ray is valid
    # The ray should extend from the camera to where it would intersect y=0 or beyond
    if angle_degrees < 0 or angle_degrees > 180:
        # Ray going upward - just show a short segment
        x_end = min(camera_x + 20, x_max)
    else:
        # Ray going downward - extend to x_max
        x_end = x_max

    # Generate points for the ray
    x_ray = np.array([camera_x, x_end])
    y_ray = camera_y + slope * (x_ray - camera_x)

    return x_ray, y_ray


def find_intersection(
    x_road: np.ndarray,
    y_road: np.ndarray,
    angle_degrees: float,
    camera_x: float = 0,
    camera_y: float = 1.5
) -> tuple[float | None, float | None, float | None]:
    """
    Find the intersection point between the camera ray and the road profile.

    Parameters
    ----------
    x_road : np.ndarray
        X-coordinates of the road profile
    y_road : np.ndarray
        Y-coordinates of the road profile
    angle_degrees : float
        Angle of the camera ray in degrees
    camera_x : float, optional
        X-coordinate of camera position (default: 0)
    camera_y : float, optional
        Y-coordinate of camera position (default: 1.5)

    Returns
    -------
    tuple[float | None, float | None, float | None]
        x, y coordinates of intersection and distance from camera,
        or (None, None, None) if no intersection found

    Examples
    --------
    >>> x_road = np.array([0, 10, 20, 30])
    >>> y_road = np.array([0, 2, 3, 4])
    >>> x, y, dist = find_intersection(x_road, y_road, -10.0)
    >>> x is not None
    True
    """
    angle_rad = -np.deg2rad(angle_degrees)

    # Handle vertical ray
    if np.abs(np.cos(angle_rad)) < 1e-10:
        return None, None, None

    slope = np.tan(angle_rad)

    # Ray equation: y = camera_y + slope * (x - camera_x)
    # Check each segment of the road for intersection
    for i in range(len(x_road) - 1):
        x1, y1 = x_road[i], y_road[i]
        x2, y2 = x_road[i + 1], y_road[i + 1]

        # Skip if this segment is behind the camera
        if x2 <= camera_x:
            continue

        # Calculate y values of the ray at x1 and x2
        ray_y1 = camera_y + slope * (x1 - camera_x)
        ray_y2 = camera_y + slope * (x2 - camera_x)

        # Check if the ray crosses the road segment
        # The ray intersects if it's on different sides of the road at x1 and x2
        diff1 = ray_y1 - y1
        diff2 = ray_y2 - y2

        if diff1 * diff2 <= 0:  # Sign change or zero indicates intersection
            # Linear interpolation to find exact intersection point
            if abs(diff2 - diff1) < 1e-10:
                # Parallel lines
                t = 0
            else:
                t = diff1 / (diff1 - diff2)

            # Interpolate to find intersection point
            x_intersect = x1 + t * (x2 - x1)
            y_intersect = y1 + t * (y2 - y1)

            # Calculate distance from camera to intersection
            distance = np.sqrt((x_intersect - camera_x)**2 + (y_intersect - camera_y)**2)

            return x_intersect, y_intersect, distance

    return None, None, None

Was wir gerade gemacht haben:

Jetzt aktualisieren Sie main.py, um das neue Modul zu verwenden:

Am Anfang von main.py, fügen Sie hinzu:

from geometry import calculate_ray_line, find_intersection

Dann löschen Sie die calculate_ray_line() und find_intersection() Funktionsdefinitionen aus main.py (sie sind jetzt in geometry.py).

Testen Sie, dass es noch funktioniert:

$ uv run road-profile-viewer
# Anwendung sollte normal starten
# Öffnen Sie Browser zu http://127.0.0.1:8050/
# Überprüfen Sie, dass Strahlenschnitt noch funktioniert

Checkpoint: Wenn die Anwendung funktioniert, haben Sie erfolgreich Geometrie extrahiert!

Jetzt committen Sie Ihren Fortschritt:

$ git add geometry.py main.py
$ git commit -m "Extract geometry functions to geometry.py

- Move calculate_ray_line() and find_intersection() to geometry.py
- Add comprehensive docstrings and type hints
- Update main.py to import from geometry module
- Application still works identically

This separates pure math functions from UI code, making them easier to test."

6.3 Schritt 3: Extrahieren Sie Straßengenerierung → road.py

Ziel: Verschieben Sie Straßengenerierungs-Logik in ihr eigenes Modul.

Öffnen Sie road.py und fügen Sie hinzu:

"""
Road profile generation.

This module handles generation of road profiles using various mathematical
curves (currently clothoid-like approximations).

All functions are pure (no side effects) and depend only on numpy.
"""

import numpy as np


def generate_road_profile(
    num_points: int = 100,
    x_max: float = 80
) -> tuple[np.ndarray, np.ndarray]:
    """
    Generate a road profile using a clothoid-like approximation.

    A clothoid (Euler spiral) is a curve whose curvature increases linearly
    with its arc length. This function approximates it with a polynomial curve.

    Parameters
    ----------
    num_points : int, optional
        Number of points to generate (default: 100)
    x_max : float, optional
        Maximum x-coordinate value (default: 80)

    Returns
    -------
    tuple[np.ndarray, np.ndarray]
        x and y coordinates of the road profile

    Examples
    --------
    >>> x, y = generate_road_profile(num_points=50, x_max=40)
    >>> len(x)
    50
    >>> x[0], y[0]
    (0.0, 0.0)
    """
    # Generate equidistant x points from 0 to x_max
    x = np.linspace(0, x_max, num_points)

    # Create a clothoid-like curve using a combination of polynomial and sinusoidal terms
    # This creates a road that starts flat and gradually curves
    # Normalize x for the calculation
    x_norm = x / x_max

    # Clothoid approximation: starts flat, gradually increases curvature
    # Scale to keep maximum height around 8m (realistic road profile)
    y = (0.015 * x_norm**3 * x_max +
         0.3 * np.sin(2 * np.pi * x_norm) +
         0.035 * x_norm * x_max)

    # Ensure it starts at (0, 0)
    y = y - y[0]

    return x, y

Aktualisieren Sie main.py:

Fügen Sie den Import hinzu:

from road import generate_road_profile

Löschen Sie die generate_road_profile() Funktionsdefinition aus main.py.

Testen Sie erneut:

$ uv run road-profile-viewer
# Überprüfen Sie, dass Straßengenerierung noch funktioniert

Checkpoint: Anwendung sollte noch perfekt funktionieren!

Committen Sie diesen Schritt:

$ git add road.py main.py
$ git commit -m "Extract road generation to road.py

- Move generate_road_profile() to road.py
- Add docstrings explaining clothoid approximation
- Update main.py to import from road module
- Application still works identically

Separates data generation from geometry and UI."

6.4 Schritt 4: Extrahieren Sie Dash UI → visualization.py

Ziel: Verschieben Sie allen Dash/UI-Code in ein dediziertes Modul.

Öffnen Sie visualization.py und fügen Sie hinzu:

"""
Visualization layer for the Road Profile Viewer.

This module contains all Dash UI components, layout definitions, and callbacks.
It orchestrates the geometry and road modules to create an interactive visualization.
"""

import numpy as np
from dash import Dash, html, dcc, Input, Output
import plotly.graph_objects as go

from geometry import calculate_ray_line, find_intersection
from road import generate_road_profile


def create_dash_app() -> Dash:
    """
    Create and configure the Dash application.

    Returns
    -------
    Dash
        Configured Dash application instance ready to run
    """
    # Initialize the Dash app
    app = Dash(__name__)

    # Define the layout
    app.layout = html.Div([
        html.H1("Road Profile Viewer with Camera Ray Intersection",
                style={'textAlign': 'center', 'color': '#2c3e50', 'marginBottom': '20px'}),

        html.Div([
            html.Label("Camera Ray Angle (degrees from horizontal):",
                      style={'fontWeight': 'bold', 'marginRight': '10px'}),
            dcc.Input(
                id='angle-input',
                type='number',
                value=-1.1,
                step=0.1,
                style={'marginRight': '20px', 'padding': '5px', 'width': '100px'}
            ),
            html.Span(id='intersection-info',
                     style={'color': '#e74c3c', 'fontWeight': 'bold'})
        ], style={'textAlign': 'center', 'marginBottom': '20px', 'padding': '10px'}),

        dcc.Graph(id='road-profile-graph', style={'height': '400px'}),

        html.Div([
            html.H3("Instructions:", style={'color': '#2c3e50'}),
            html.Ul([
                html.Li("The dark grey line represents the road profile"),
                html.Li("The red point at (0, 2.0) represents the camera position"),
                html.Li("The blue line shows the camera ray at the specified angle"),
                html.Li("The green point shows where the ray intersects the road"),
                html.Li("Hover over the green point to see the distance from camera to intersection"),
                html.Li("Adjust the angle to see how the intersection point changes"),
                html.Li("Negative angles point downward, positive angles point upward")
            ])
        ], style={'margin': '20px', 'padding': '20px', 'backgroundColor': '#ecf0f1', 'borderRadius': '5px'})
    ])

    # Define the callback to update the graph
    @app.callback(
        [Output('road-profile-graph', 'figure'),
         Output('intersection-info', 'children')],
        [Input('angle-input', 'value')]
    )
    def update_graph(angle):
        """
        Update the graph based on the input angle.

        Parameters
        ----------
        angle : float
            Camera ray angle in degrees

        Returns
        -------
        tuple
            (plotly figure, info text)
        """
        if angle is None:
            angle = -1.1

        # Generate road profile
        x_road, y_road = generate_road_profile(num_points=100, x_max=80)

        # Camera position
        camera_x, camera_y = 0, 2.0

        # Find intersection first to determine ray length
        x_intersect, y_intersect, distance = find_intersection(
            x_road, y_road, angle, camera_x, camera_y
        )

        # Calculate adaptive ray line based on intersection
        if x_intersect is not None:
            # Ray goes from camera to intersection point
            x_ray = np.array([camera_x, x_intersect])
            y_ray = np.array([camera_y, y_intersect])
        else:
            # No intersection - show a short ray (20 units or to edge of plot)
            angle_rad = -np.deg2rad(angle)
            if np.abs(np.cos(angle_rad)) < 1e-10:
                # Vertical line
                x_ray = np.array([camera_x, camera_x])
                y_ray = np.array([camera_y, camera_y - 10])
            else:
                slope = np.tan(angle_rad)
                x_end = min(camera_x + 20, 80)
                y_end = camera_y + slope * (x_end - camera_x)
                x_ray = np.array([camera_x, x_end])
                y_ray = np.array([camera_y, y_end])

        # Create figure
        fig = go.Figure()

        # Add road profile
        fig.add_trace(go.Scatter(
            x=x_road,
            y=y_road,
            mode='lines+markers',
            name='Road Profile',
            line=dict(color='#4a4a4a', width=3),
            marker=dict(size=4, color='#4a4a4a'),
            hovertemplate='Road<br>x: %{x:.2f}<br>y: %{y:.2f}<extra></extra>'
        ))

        # Add camera point
        fig.add_trace(go.Scatter(
            x=[camera_x],
            y=[camera_y],
            mode='markers',
            name='Camera',
            marker=dict(size=12, color='red', symbol='circle'),
            hovertemplate='Camera<br>Position: (%{x:.2f}, %{y:.2f})<extra></extra>'
        ))

        # Add camera ray
        fig.add_trace(go.Scatter(
            x=x_ray,
            y=y_ray,
            mode='lines',
            name=f'Camera Ray ({angle}°)',
            line=dict(color='blue', width=2, dash='dash'),
            hovertemplate='Camera Ray<br>x: %{x:.2f}<br>y: %{y:.2f}<extra></extra>'
        ))

        # Add intersection point if it exists
        info_text = ""
        if x_intersect is not None:
            fig.add_trace(go.Scatter(
                x=[x_intersect],
                y=[y_intersect],
                mode='markers',
                name='Intersection',
                marker=dict(size=15, color='green', symbol='star'),
                hovertemplate=f'Intersection Point<br>Position: ({x_intersect:.2f}, {y_intersect:.2f})<br>Distance from camera: {distance:.2f}<extra></extra>'
            ))
            info_text = f"Intersection found at ({x_intersect:.2f}, {y_intersect:.2f}) | Distance: {distance:.2f} units"
        else:
            info_text = "No intersection found with current angle"

        # Update layout
        fig.update_layout(
            xaxis_title="X Position (m)",
            yaxis_title="Y Position (m)",
            hovermode='closest',
            showlegend=True,
            legend=dict(
                x=1.02,
                y=1,
                xanchor='left',
                yanchor='top',
                bgcolor='rgba(255,255,255,0.8)',
                bordercolor='#dee2e6',
                borderwidth=1
            ),
            plot_bgcolor='#f8f9fa',
            xaxis=dict(
                gridcolor='#dee2e6',
                range=[-2, 82],
                constrain='domain'
            ),
            yaxis=dict(
                gridcolor='#dee2e6',
                scaleanchor='x',
                scaleratio=1,
                range=[-0.5, 10],
                constrain='domain'
            ),
            margin=dict(l=50, r=150, t=30, b=50)
        )

        return fig, info_text

    return app

Aktualisieren Sie main.py:

Ihre main.py sollte jetzt sehr einfach sein:

"""
Road Profile Viewer - Entry Point

This is the main entry point for the Road Profile Viewer application.
All functionality is implemented in separate modules:
- geometry.py: Ray intersection calculations
- road.py: Road profile generation
- visualization.py: Dash UI and visualization

To run the application:
    $ uv run road-profile-viewer
"""

from visualization import create_dash_app


def main():
    """
    Main function to run the Dash application.
    """
    app = create_dash_app()
    print("Starting Road Profile Viewer...")
    print("Open your browser and navigate to: http://127.0.0.1:8050/")
    print("Press Ctrl+C to stop the server.")
    app.run(debug=True)


if __name__ == '__main__':
    main()

Testen Sie das vollständige Refactoring:

$ uv run road-profile-viewer
# Überprüfen Sie dass ALLES noch funktioniert:
# - Straße rendert korrekt
# - Strahlenwinkel passt sich an
# - Schnittpunkt aktualisiert sich
# - Distanz wird angezeigt

Checkpoint: Wenn alles funktioniert, haben Sie Ihren Monolithen erfolgreich refactored!

Committen Sie diesen großen Schritt:

$ git add visualization.py main.py
$ git commit -m "Extract UI layer to visualization.py and simplify main.py

- Move create_dash_app() and all UI code to visualization.py
- Simplify main.py to just entry point (~20 lines)
- visualization.py imports from geometry and road modules
- Application works identically to before refactoring

Completes modular refactoring: main → visualization → geometry/road"

7. Das Refactoring überprüfen

7.1 Überprüfen Sie Ihre Modul-Struktur

$ ls *.py
geometry.py  main.py  road.py  visualization.py

7.2 Überprüfen Sie Zeilenzahlen

$ wc -l *.py
   175 geometry.py     # Reine Geometrie-Berechnungen
    67 road.py         # Straßengenerierung
   203 visualization.py # Aller UI-Code
    20 main.py         # Nur der Einstiegspunkt
   ---
   465 total

Beachten Sie:

7.3 Überprüfen Sie den Abhängigkeitsfluss

# main.py importiert:
from visualization import create_dash_app

# visualization.py importiert:
from geometry import calculate_ray_line, find_intersection
from road import generate_road_profile

# geometry.py importiert:
import numpy as np  # Nur externe Abhängigkeit

# road.py importiert:
import numpy as np  # Nur externe Abhängigkeit

Abhängigkeits-Graph:

main.py
   ↓
visualization.py
   ↓         ↓
geometry.py  road.py
   ↓         ↓
  numpy    numpy

Einseitige Abhängigkeiten - keine Zyklen!


8. Teil 6: Vorteile, die Sie gerade gewonnen haben

8.1 Einfache Navigation

Vorher:

# "Wo ist die Schnittpunkt-Berechnung?"
# *Scrollt durch 390 Zeilen*

Nachher:

# "Wo ist die Schnittpunkt-Berechnung?"
# Öffnet geometry.py → find_intersection() ist direkt da

8.2 Fokussierte Änderungen

Vorher:

# Änderung des Straßengenerierungs-Algorithmus
# Datei: main.py (390 Zeilen - könnte versehentlich UI brechen)

Nachher:

# Änderung des Straßengenerierungs-Algorithmus
# Datei: road.py (67 Zeilen - kann UI-Code nicht versehentlich anfassen)

8.3 Bessere Zusammenarbeit

Vorher:

Student A: Bearbeitet main.py Zeilen 50-120 (Straßengenerierung)
Student B: Bearbeitet main.py Zeilen 200-350 (UI-Styling)
Ergebnis: MERGE-KONFLIKT (obwohl Änderungen unabhängig sind!)

Nachher:

Student A: Bearbeitet road.py
Student B: Bearbeitet visualization.py
Ergebnis: Keine Konflikte! Verschiedene Dateien können gleichzeitig bearbeitet werden.

8.4 Testbarkeit (Vorschau auf Kapitel 03)

Vorher:

# Um find_intersection() zu testen
from main import find_intersection  # Importiert ALLES
# Dash startet, langsame Imports, potenzielle Seiteneffekte

Nachher:

# Um find_intersection() zu testen
from geometry import find_intersection  # Importiert nur Geometrie
# Schnell, fokussiert, keine Seiteneffekte

8.5 Wiederverwendbarkeit

Vorher:

# Wollen Sie find_intersection() in einem anderen Projekt verwenden?
# Copy-Paste aus main.py → bringt unnötigen Dash-Code mit

Nachher:

# Wollen Sie find_intersection() in einem anderen Projekt verwenden?
# Kopieren Sie einfach geometry.py → sauberes, eigenständiges Modul

9. Teil 7: Häufige Fallstricke und Lösungen

9.1 Fallstrick 1: Zirkuläre Imports

Der Fehler:

ImportError: cannot import name 'generate_road_profile' from partially initialized module 'road'
(most likely due to a circular import)

Die Ursache:

# visualization.py
from road import generate_road_profile

# road.py (SCHLECHT!)
from visualization import create_dash_app  # Warum sollte road.py UI-Code brauchen??

Die Lösung:

9.2 Fallstrick 2: Unklare Modul-Grenzen

Die Verwirrung:

# Wo gehören camera_x und camera_y hin?
# - geometry.py? (wird in Berechnungen verwendet)
# - visualization.py? (ist ein UI-Parameter)
# - Eine neue config.py Datei?

Die Lösung:

9.3 Fallstrick 3: Über-Refactoring

Die Versuchung:

Sollte ich erstellen:
- geometry_helpers.py?
- geometry_utils.py?
- ray_calculations.py (getrennt von geometry.py)?
- intersection_math.py?

Die Lösung:

9.4 Fallstrick 4: Vergessen Imports zu aktualisieren

Der Fehler:

# In main.py
def main():
    x, y = generate_road_profile()  # NameError: generate_road_profile not defined

Die Lösung:

# Fügen Sie den Import am Anfang hinzu
from road import generate_road_profile

Pro-Tipp: Nutzen Sie die Auto-Import-Funktion Ihrer IDE (Ctrl+. in VS Code), um Imports automatisch hinzuzufügen.


10. Teil 8: Ihre CI-Checks ausführen

Ihr Refactoring sollte alle existierenden CI-Checks bestehen!

# Führen Sie Ruff aus (Style-Check)
$ uv run ruff check .
All checks passed!

# Führen Sie Ruff Format-Check aus
$ uv run ruff format --check .
All files formatted correctly!

# Führen Sie Pyright aus (Type-Check)
$ uv run pyright
0 errors, 0 warnings

Wenn Sie Fehler bekommen:

Ruff-Fehler:

Pyright-Fehler:


11. Ihren Feature-Branch pushen und einen Pull Request erstellen

Sie haben nach jedem Schritt committed. Jetzt ist es Zeit, Ihren Branch zu pushen und einen PR zu erstellen!

11.1 Überprüfen Sie Ihre Commits

$ git log --oneline
7h8i9j0 Extract UI layer to visualization.py and simplify main.py
d4e5f6g Extract road generation to road.py
a1b2c3d Extract geometry functions to geometry.py

Saubere, logische Progression! Jeder Commit repräsentiert einen fokussierten Refactoring-Schritt.

11.2 Pushen Sie zu GitHub

$ git push -u origin feature/refactor-to-modules

Das lädt alle Ihre Commits zu GitHub hoch.

11.3 Erstellen Sie Pull Request

$ gh pr create --title "Refactor: Split monolithic main.py into focused modules" \
  --body "Refactors monolithic main.py (390 lines) into four focused modules:

**New Structure:**
- geometry.py: Pure math functions (ray intersection)
- road.py: Road generation logic
- visualization.py: Dash UI layer
- main.py: Entry point only (20 lines)

**Benefits:**
- Easier navigation and testing
- Reduced merge conflicts
- Better code organization
- Prepares for Chapter 03 (Testing Fundamentals) (testing)

**Verification:**
- All Ruff/Pyright checks pass
- Application works identically
- Clean dependency flow (no cycles)"

Oder erstellen Sie den PR über die GitHub-Weboberfläche, wenn Sie das bevorzugen.

11.4 Warte auf CI-Checks

GitHub Actions wird automatisch laufen:

✅ Ruff check (style)
✅ Ruff format check
✅ Pyright (types)

Alle sollten bestehen, da Sie sie lokal ausgeführt haben!


12. KI-Coding-Assistenten für Refactoring nutzen

12.1 Der Workflow des modernen Entwicklers

Wichtiger Hinweis: Bevor Sie KI-Assistenten für Refactoring verwenden, MÜSSEN Sie die Prinzipien verstehen, die wir gerade gelernt haben:

Warum das wichtig ist: Ohne das Verständnis dieser Prinzipien können Sie nicht:

Die KI ist ein Werkzeug, kein Ersatz für Ihr Wissen!

12.2 Wie KI Refactoring beschleunigen kann

Sobald Sie die Prinzipien verstehen, können KI-Coding-Assistenten helfen bei:

  1. Mechanischer Code-Verschiebung - Funktionen zwischen Dateien verschieben
  2. Import-Aktualisierung - Import-Anweisungen hinzufügen/entfernen
  3. Boilerplate-Code schreiben - Modul-Docstrings, Type Hints
  4. Konsistenz wahren - Sicherstellen, dass alle Referenzen aktualisiert werden

Was KI nicht kann:

12.3 Beispiel: KI-gestützter Refactoring-Prompt

Szenario: Sie haben Ihre Refactoring-Strategie geplant (manuell!) und möchten jetzt, dass die KI bei der Ausführung hilft.

Effektiver Prompt:

Sie werden die monolithische `src/road_profile_viewer/main.py`
(390 Zeilen) in vier fokussierte Module refactoren:

- geometry.py - Reine Mathe-Funktionen (Strahlenschnitt-Berechnungen)
- road.py - Straßenprofil-Generierung
- visualization.py - Dash UI-Schicht
- main.py - Vereinfachter Einstiegspunkt (~20 Zeilen)

Erstellen Sie einen Feature-Branch namens feature/refactor-to-modules und
machen Sie 4 inkrementelle Commits (einen pro Schritt).

---

Schritt 1: Geometrie-Modul extrahieren

Erstellen Sie geometry.py im Verzeichnis src/road_profile_viewer/.

Anforderungen:
- Kopieren Sie calculate_ray_line() und find_intersection() aus main.py
- Fügen Sie korrekte Imports hinzu: import numpy as np
- Behalten Sie alle Docstrings und Type Hints
- Fügen Sie einen Modul-Docstring hinzu, der erklärt, dass dies das Geometrie-Modul ist

Commit-Message-Format:
"refactor: extract geometry functions to separate module

Extract calculate_ray_line() and find_intersection() from main.py
into a new geometry.py module to improve code organization.

This is step 1 of refactoring the monolithic main.py (390 lines)
into focused modules."

---

Schritt 2: Road-Modul extrahieren

Erstellen Sie road.py im Verzeichnis src/road_profile_viewer/.

Anforderungen:
- Kopieren Sie generate_road_profile() aus main.py
- Fügen Sie korrekte Imports hinzu: import numpy as np
- Behalten Sie alle Docstrings
- Fügen Sie einen Modul-Docstring hinzu

Commit-Message-Format:
"refactor: extract road profile generation to separate module

Extract generate_road_profile() from main.py into road.py
to improve separation of concerns.

This is step 2 of refactoring the monolithic main.py."

---

Schritt 3: Visualization-Modul extrahieren

Erstellen Sie visualization.py im Verzeichnis src/road_profile_viewer/.

Anforderungen:
- Kopieren Sie create_dash_app() und ALLEN UI-Code aus main.py
- Fügen Sie Imports für Dash, plotly hinzu
- Verwenden Sie ABSOLUTE Imports (nicht relativ):
  from road_profile_viewer.geometry import find_intersection
  from road_profile_viewer.road import generate_road_profile
- Fügen Sie einen Modul-Docstring hinzu

Warum absolute Imports? Beim direkten Ausführen des Moduls scheitern
relative Imports mit ImportError. Absolute Imports funktionieren in allen Kontexten.

Commit-Message-Format:
"refactor: extract Dash UI layer to visualization module

Extract create_dash_app() and all UI code from main.py into
visualization.py using absolute imports.

This is step 3 of refactoring. The visualization module now
handles all Dash UI components and callbacks."

---

Schritt 4: main.py vereinfachen

Ersetzen Sie main.py mit einer vereinfachten Version (~26 Zeilen).

Anforderungen:
- Behalten Sie nur die Einstiegspunkt-Funktionalität
- Verwenden Sie ABSOLUTE Import:
  from road_profile_viewer.visualization import create_dash_app
- Behalten Sie main() Funktion und if __name__ == "__main__": Block
- Fügen Sie prägnanten Modul-Docstring hinzu

Template:
"""
Road Profile Viewer - Interactive 2D Visualization
Main entry point for the road profile viewer application.
"""

from road_profile_viewer.visualization import create_dash_app

def main():
    app = create_dash_app()
    print("Starting Road Profile Viewer...")
    print("Open your browser: http://127.0.0.1:8050/")
    app.run(debug=True)

if __name__ == "__main__":
    main()

Commit-Message-Format:
"refactor: simplify main.py to entry point only

Refactor main.py from 390 lines to 26 lines by extracting
implementation to separate modules (geometry.py, road.py,
visualization.py).

This completes the refactoring with clear separation of concerns."

12.4 Warum dieser Prompt funktioniert

Klare Struktur:

Enthält Kontext:

Ermöglicht Verifikation:

12.5 Nach der Code-Generierung durch KI: Ihre Verantwortung

Akzeptieren Sie niemals blind von der KI generierten Code! Immer:

  1. Jede Zeile lesen - Verstehen, was sich geändert hat
  2. Nach jedem Schritt testen - uv run road-profile-viewer ausführen
  3. Imports überprüfen - Auf zirkuläre Abhängigkeiten prüfen
  4. CI-Checks ausführen - Sicherstellen, dass Ruff, Pyright bestehen
  5. Commits überprüfen - git diff auf unerwartete Änderungen prüfen
  6. Struktur validieren - Entspricht es Ihrem Architekturplan?

Warnzeichen, auf die Sie achten sollten:

12.6 Das Mensch-KI-Kollaborationsmodell

Effektives Refactoring mit KI:

Mensch: Strategisches Denken
├── Monolith-Probleme identifizieren
├── Modul-Grenzen entscheiden
├── Abhängigkeitshierarchie planen
└── Erfolgskriterien definieren
        ↓
KI: Taktische Ausführung
├── Code zwischen Dateien verschieben
├── Import-Anweisungen aktualisieren
├── Boilerplate-Code schreiben
└── Commit-Messages formatieren
        ↓
Mensch: Verifikation & Qualität
├── Funktionalität testen
├── Architektur überprüfen
├── Nach subtilen Bugs suchen
└── Gegen CI validieren

Sie sind der Architekt. Die KI ist die Baufirma.

12.7 Lernübung: Zuerst ohne KI üben

Empfehlung für Studenten:

  1. Erstes Mal: Refactoring manuell durchführen (heutige Übung)
    • Sie werden die Herausforderungen tiefgreifend verstehen
    • Intuition für mögliche Probleme aufbauen
    • Lernen, zirkuläre Abhängigkeiten zu erkennen
  2. Zweites Mal: KI-Unterstützung verwenden (zukünftiges Projekt)
    • Jetzt können Sie die Vorschläge der KI bewerten
    • Sie werden ihre Fehler erkennen
    • Sie werden bessere Prompts schreiben

Warum diese Reihenfolge wichtig ist: Sie können ein Werkzeug nicht effektiv nutzen, das Sie nicht verstehen. Lernen Sie zuerst die Prinzipien, dann nutzen Sie die Automatisierung.


17. Zusammenfassung: Was Sie erreicht haben

17.1 Vor dem Refactoring

main.py (390 Zeilen)
└── Alles durcheinander:
    ├── Geometrie-Mathe
    ├── Straßengenerierung
    ├── Dash UI
    └── Anwendungsstart

Probleme:

17.2 Nach dem Refactoring

├── geometry.py (175 Zeilen) - Reine Mathe-Funktionen
├── road.py (67 Zeilen) - Datengenerierung
├── visualization.py (203 Zeilen) - UI-Schicht
└── main.py (20 Zeilen) - Einstiegspunkt

Vorteile:

17.3 Schlüsselprinzipien, die Sie gelernt haben

  1. Separation of Concerns (Trennung der Zuständigkeiten)
    • Jedes Modul macht eine Sache gut
    • Reine Funktionen getrennt von UI-Code
  2. Abhängigkeits-Richtung
    • Module niedrigerer Ebene (geometry, road) importieren nicht aus höherer Ebene (visualization)
    • Verhindert zirkuläre Abhängigkeiten
  3. Der “3-Wort-Beschreibung”-Test
    • Wenn Sie den Zweck eines Moduls nicht in 3 Wörtern beschreiben können, ist es nicht fokussiert genug
  4. YAGNI (“You Ain’t Gonna Need It”)
    • Erstellen Sie keine Module/Abstraktionen bis Sie sie wirklich brauchen
    • Beginnen Sie einfach, refactoren Sie wenn nötig

17.4 Was kommt als Nächstes?

Kapitel 03 (Grundlagen des Testens): Testing Fundamentals

Jetzt, wo Ihr Code modular ist, können Sie:

Die Vorbereitung ist komplett: Ihr Code ist jetzt strukturiert für professionelle Test-Driven Development.


18. Wichtige Erkenntnisse

Erinnern Sie sich an diese Prinzipien:

  1. Monolithen wachsen organisch - Eine Datei ist anfangs in Ordnung, wird aber schnell zum Problem
  2. Refactoring ist NICHT Neuschreiben - Gleiche Funktionalität, bessere Struktur
  3. Module sollten klare Grenzen haben - Reine Funktionen vs. UI-Code
  4. Abhängigkeiten sollten in eine Richtung fließen - Verhindert zirkuläre Imports
  5. Testbarkeit ergibt sich aus Modularität - Kann nicht testen was Sie nicht importieren können
  6. Der “3-Wort-Test” - Können Sie den Zweck des Moduls prägnant beschreiben?
  7. YAGNI - Nicht über-engineeren. Beginnen Sie einfach, refactoren Sie wenn nötig.
  8. CI sollte bestehen - Refactoring ändert nicht die Funktionalität, also bestehen Tests weiterhin

Sie sind jetzt bereit für Kapitel 03 (Grundlagen des Testens): Testing!

Ihre modulare Code-Struktur macht Testen nicht nur möglich, sondern natürlich und einfach.


19. Weiterführende Literatur

Über Refactoring:

Über Modul-Design:

Über Python-Modul-Organisation:

Interaktives Lernen:


Zuletzt aktualisiert: 2025-10-21 Voraussetzungen: Vorlesungen 1-3 (Git, Code Quality, CI/CD) Nächste Vorlesung: Kapitel 03 (Grundlagen des Testens) - Testing Fundamentals

© 2026 Dominik Mueller   •  Powered by Soopr   •  Theme  Moonwalk