Home

06 Softwarearchitektur Teil 1: Grundlagen und Architektursichten

lecture architecture non-functional-requirements 4+1-views design-decisions trade-offs

1. Einführung: Ihre Sprints sind schnell, aber kann Ihr System skalieren?

In Teil 1 und Teil 2 der agilen Entwicklung haben wir gelernt:

Ihr Road Profile Viewer-Team hat schnell Features geliefert. Die Sprint Review Demos sind beeindruckend. Der Product Owner ist zufrieden. Die CI-Pipeline ist grün.

Dann fragt der Stakeholder: “Können wir das für 1.000 gleichzeitige Benutzer in drei europäischen Büros bereitstellen?”

Plötzlich wird Ihnen klar:

Der agile Prozess funktioniert. Die Architektur nicht.

Dieses Szenario ist nicht hypothetisch. Twitter erlebte dies bekanntermaßen 2008-2010: Ihre monolithische Ruby on Rails-Anwendung konnte das explosive Benutzerwachstum nicht bewältigen. Die “Fail Whale”-Fehlerseite wurde ikonisch — ein Symbol dafür, was passiert, wenn die Architektur eines Systems seinen Erfolg nicht unterstützen kann.

1.1 Was ist Softwarearchitektur?

Softwarearchitektur beschreibt, wie ein System als eine Menge kommunizierender Komponenten organisiert ist und wie sich diese Komponenten verhalten und interagieren.

Architektur ist die grundlegende Organisation, die alles beeinflusst — von der Geschwindigkeit des Systems bis hin zur Einfachheit, mit der es geändert werden kann. Sie beantwortet Fragen wie:

1.2 Das Architekturmodell

Ein Architekturmodell ermöglicht:

Vorteil Beschreibung Beispiel
Stakeholder-Kommunikation Bietet ein gemeinsames Verständnis für technische und nicht-technische Stakeholder Blockdiagramme, die zeigen, wie der Road Profile Viewer mit der Datenbank verbunden ist
Systemanalyse Ermöglicht die Bewertung gegen kritische Anforderungen vor der Implementierung "Wenn wir 1.000 Benutzer hinzufügen, welche Komponente wird zuerst zum Engpass?"
Wiederverwendung Ermöglicht die Nutzung vorhandener Lösungen und Muster Verwendung des MVC-Musters, weil es in Millionen von Web-Apps bewährt ist
Dokumentation Dokumentiert Designentscheidungen und deren Begründung für zukünftige Entwickler "Wir haben SQLite für Einfachheit gewählt; bei Skalierung auf PostgreSQL migrieren"

1.3 Die Brücke: Agile trifft Architektur

Agile sagt uns, wie wir Arbeit organisieren. Architektur sagt uns, wie wir das System selbst organisieren. Beides muss zusammenarbeiten.

Einige Entwickler glauben, Agile bedeute “kein Vorab-Design” — einfach coden und refaktorieren. Das ist ein Missverständnis. Selbst die agilsten Teams brauchen frühe Architekturentscheidungen:

Der Unterschied ist, dass agile Architektur sich durch iterative Verfeinerung entwickelt, anstatt vollständig im Voraus spezifiziert zu werden. Wir treffen genug Architekturentscheidungen, um zu starten, und verfeinern dann, während wir lernen.


2. Lernziele

Am Ende dieser Vorlesung (Teil 1) werden Sie in der Lage sein:

  1. Die Motivation und Ziele für architektonisches Design zu benennen — warum Architektur wichtig ist und was ein Architekturmodell ermöglicht
  2. Die Abhängigkeit zwischen Systemstruktur und nicht-funktionalen Anforderungen zu erklären — wie Performance, Sicherheit und Wartbarkeit architektonische Entscheidungen prägen
  3. Das 4+1-Sichtenmodell anzuwenden, um ein System aus mehreren Perspektiven zu beschreiben — Logisch, Prozess, Entwicklung, Physisch und Szenarien

Teil 2 wird Architekturmuster (MVC, Schichtenarchitektur, verteilte Systeme) behandeln und wie man sie auf den Road Profile Viewer anwendet.


3. Architektonische Designentscheidungen

3.1 Architektur als kreativer Prozess

Architektonisches Design ist grundlegend kreativ. Anders als Algorithmen mit beweisbar optimalen Lösungen beinhaltet Architektur Kompromisse, die abhängen von:

“Es gibt keinen formelhaften architektonischen Designprozess. Er hängt vom Typ des zu entwickelnden Systems ab, vom Hintergrund und der Erfahrung des Systemarchitekten und von den spezifischen Anforderungen an das System.” — Sommerville, Software Engineering

Aus diesem Grund ist es am besten, architektonisches Design als eine Reihe zu treffender Entscheidungen zu betrachten, anstatt als eine Abfolge von Schritten.

3.2 Zentrale Designfragen

Während des architektonischen Designs müssen Architekten diese grundlegenden Fragen berücksichtigen:

?
1 📋 Gibt es eine generische Architekturvorlage?
2 🖥️ Wie wird es auf Hardware verteilt?
3 🧩 Welche Architekturmuster könnten verwendet werden?
4 🏗️ Was ist der grundlegende Strukturierungsansatz?
5 📦 Wie werden Komponenten zerlegt?
6 🎛️ Welche Strategie steuert den Komponentenbetrieb?
7 Welche Organisation liefert NFRs am besten?
8 📝 Wie sollte die Architektur dokumentiert werden?
Vorlagen & Muster
Systemstruktur
Nicht-funktionale Anforderungen
Dokumentation

Für den Road Profile Viewer, bedenken Sie:

3.3 Nicht-funktionale Anforderungen treiben die Architektur

Die wichtigste Erkenntnis im architektonischen Design ist diese:

Architekturentscheidungen hängen stark von nicht-funktionalen Anforderungen ab, und umgekehrt beeinflussen sie die nicht-funktionalen Eigenschaften des Systems.

Ihre Wahl der Architektur bestimmt, was das System kann, nicht nur was es tut.

Nicht-funktionale Anforderung Architektonische Implikation Beispiel
Performance Kritische Operationen in wenigen großen Komponenten lokalisieren; auf derselben Maschine bereitstellen, um Netzwerklatenz zu minimieren Game-Engines halten Physik und Rendering in einer engen Schleife auf der GPU
Sicherheit Schichtenarchitektur verwenden mit kritischsten Assets in innersten Schichten; hohe Sicherheitsvalidierung auf innere Schichten anwenden Banksysteme: öffentliches Web → API-Gateway → Geschäftslogik → verschlüsselte Datenbank
Safety Sicherheitskritische Operationen in einer einzelnen Komponente oder kleinem Set zusammenfassen; ermöglicht fokussierte Validierung und Schutzsysteme Flugzeugsteuerung: sicherheitskritischer Code isoliert, kann formal verifiziert werden
Verfügbarkeit Redundante Komponenten einschließen; Austausch und Updates ohne Systemstopp ermöglichen Netflix: mehrere Instanzen jedes Services; ein Ausfall legt nicht die gesamte Site lahm
Wartbarkeit Feingranulare, eigenständige Komponenten verwenden; Datenproduzenten von Konsumenten trennen; gemeinsame Datenstrukturen vermeiden Microservices: jedes Team besitzt seinen Service, kann ändern ohne Koordination

3.3.1 Performance-getriebene Architektur

Wenn Performance das Hauptanliegen ist, muss die Architektur Latenz minimieren und Durchsatz maximieren. Dies prägt jede Entscheidung — von der Gruppierung der Komponenten bis zu ihrem physischen Ausführungsort.

Kernprinzipien für Performance:

  1. Kritische Operationen lokalisieren: Performance-sensitiven Code in so wenigen Komponenten wie möglich halten, um Kommunikations-Overhead zu minimieren
  2. Netzwerk-Hops minimieren: Jeder Netzwerkaufruf fügt Latenz hinzu (typischerweise 1-100ms); lokale Funktionsaufrufe sind Nanosekunden
  3. Daten und Berechnung zusammenlegen: Daten dort verarbeiten, wo sie liegen; keine großen Datensätze über Grenzen verschieben
  4. Größere, monolithische Komponenten verwenden: Weniger, größere Komponenten bedeuten weniger Inter-Komponenten-Kommunikation

Beispiel: Web-Anwendungs-Performance

# Performance-optimized: Keep related operations together
class ProfileAnalyzer:
    """All performance-critical calculations in one place."""

    def __init__(self, profile_data: list[float]):
        # Cache data locally to avoid repeated fetches
        self._data = profile_data
        self._cached_stats = None

    def analyze(self) -> dict:
        """Single method performs all calculations in memory."""
        if self._cached_stats is None:
            # Do all computation in one pass
            self._cached_stats = {
                "mean": sum(self._data) / len(self._data),
                "max": max(self._data),
                "min": min(self._data),
                "range": max(self._data) - min(self._data),
            }
        return self._cached_stats

Dies hält alle Berechnungen in einer einzelnen Klasse mit gecachten Ergebnissen — keine Netzwerkaufrufe, keine Datenbankabfragen während der Analyse.

Echtzeit- und Embedded-Systeme

Performance-Anforderungen werden kritisch in Echtzeitsystemen, wo Antworten innerhalb strikter Zeitgrenzen erfolgen müssen. Eine Deadline zu verpassen ist nicht nur langsam — es ist ein Fehler.

Systemtyp Timing-Anforderung Architektonische Implikation
Harte Echtzeit Deadlines dürfen niemals verpasst werden Vorhersagbare Ausführungspfade, keine Garbage Collection, dedizierte Hardware
Weiche Echtzeit Gelegentliche Deadline-Verpassungen akzeptabel Prioritäts-Scheduling, begrenzte Queues, graceful Degradation
Near Realtime Schnelle Antwort erwartet (< 100ms) Caching, asynchrone Verarbeitung, Load Balancing

Fallstudie: Autonomes Fahrzeug-Architektur

Autonome Fahrzeuge gehören zu den komplexesten performance-kritischen Systemen, mit Dutzenden von Embedded-Geräten, die in Echtzeit zusammenarbeiten müssen:

Architektur Autonomer Fahrzeuge
📡 LiDAR 10-20 Hz
📷 Kamera 30-60 Hz
📶 Radar 20-77 Hz
🛰️ GPS/GNSS 10 Hz
🔀 Sensorfusions-ECU ⚡ < 50ms
Kombiniert alle Sensordaten zu einheitlichem Weltmodell
🧠 Perzeption & Planung 🎮 GPU
Objekterkennung • Pfadplanung • Entscheidungsfindung
CAN Bus / Ethernet TSN
🎯 Lenkung ECU 1 kHz
🛑 Bremsung ECU 1 kHz
Gas ECU 100 Hz
Sensoren
Verarbeitung
Aktoren
Sicherheitskritisch (1 kHz)

Hinweis: Dieses Diagramm zeigt eine vereinfachte, illustrative Architektur für Bildungszwecke. Es ist nicht von einem spezifischen Produktionsfahrzeug abgeleitet. Echte autonome Fahrzeugarchitekturen variieren erheblich zwischen Herstellern und entwickeln sich kontinuierlich weiter, da Sensortechnologie, Rechenkapazitäten und Sicherheitsstandards voranschreiten. Die Architekturlandschaft in diesem Bereich ist breit — von Teslas vision-only-Ansatz bis zu Waymos Multi-Sensor-Fusionsstrategie — und bleibt ein aktives Forschungs- und Innovationsgebiet.

Warum diese Architektur?

Kontrast: Microservices würden hier versagen

Stellen Sie sich vor, ein autonomes Fahrzeug würde Web-Style-Microservices verwenden:

❌ SCHLECHT: HTTP-Anfrage an "Brems-Service" → 50-200ms Netzwerklatenz
            Das Auto fährt 2,8 Meter bei 100 km/h während dieser Zeit!

✓ GUT: Direkte CAN-Bus-Nachricht → < 1ms Latenz
       Das Auto fährt 2,8 Zentimeter

Robotik-Beispiel: Industrieroboterarm

# Real-time control loop for robot arm (runs at 1 kHz)
class RobotArmController:
    """
    All control logic in single tight loop.
    No network calls, no disk I/O, no dynamic memory allocation.
    """

    def __init__(self):
        # Pre-allocate all memory at startup
        self.joint_positions = [0.0] * 6  # 6-axis robot
        self.target_positions = [0.0] * 6
        self.motor_commands = [0.0] * 6

    def control_cycle(self) -> None:
        """
        Called every 1ms. MUST complete within 1ms.
        Any delay causes jerky motion or safety shutdown.
        """
        # Read sensors (direct hardware access, no abstraction layers)
        self._read_encoders()

        # Compute control signals (pure math, no I/O)
        for i in range(6):
            error = self.target_positions[i] - self.joint_positions[i]
            self.motor_commands[i] = self._pid_control(i, error)

        # Write to motors (direct hardware access)
        self._write_motors()

    def _pid_control(self, joint: int, error: float) -> float:
        """PID controller - must be deterministic and fast."""
        # Simplified PID (actual implementation has more terms)
        return error * self.kp[joint]

Wichtige Architekturentscheidungen für Echtzeit:

Performance-Architektur-Strategien

Strategie Wann zu verwenden Beispiel
Monolithischer Kern Kritischer Pfad muss schnell sein Game-Engines halten Physik + Rendering zusammen
Caching-Schichten Wiederholte teure Operationen Redis-Cache vor der Datenbank
Asynchrone Verarbeitung Nicht-kritische Arbeit kann verzögert werden E-Mail-Versand in Queue statt blockierend
Edge Computing Latenz zur Cloud ist zu hoch Sensordaten lokal auf IoT-Gerät verarbeiten
Hardware-Beschleunigung Berechnung ist der Engpass GPU für ML-Inferenz, FPGA für Signalverarbeitung

Trade-off: Performance-optimierte Architekturen opfern Modularität und Testbarkeit. Eng gekoppelte, monolithische Komponenten sind schnell, aber schwer zu ändern. Wählen Sie diesen Ansatz nur, wenn Performance-Anforderungen es erfordern.

3.3.2 Sicherheitsgetriebene Architektur

Wenn Sicherheit kritisch ist, nutzen Sie Defense in Depth:

🛡️ Defense in Depth Architektur 🔐
🌐
Öffentliches Internet
⚠️ Nicht vertrauenswürdige Zone
1
🔥 Web Application Firewall
🚫 Bösartige Anfragen blockieren ⏱️ Rate Limiting
2
🚪 API Gateway
🎫 JWT-Authentifizierung Anfragenvalidierung
3
⚙️ Geschäftslogik
👤 Rollenbasierte Autorisierung 🧹 Input-Sanitisierung
4
🔌 Datenzugriffsschicht
🔒 Verschlüsselte Verbindungen 💉 SQL-Injection-Prävention
🗄️
Datenbank
🔐 Verschlüsselt im Ruhezustand

Jede Schicht fügt Schutz hinzu; ein Angreifer muss alle fünf durchbrechen, um auf Daten zuzugreifen.

Interesse an Sicherheit? Dieser Überblick kratzt nur an der Oberfläche der Sicherheitsarchitektur. Wenn Sie dieses Thema faszinierend finden, bietet die Hochschule Aalen in kommenden Semestern dedizierte Sicherheitskurse an — oder Sie könnten sogar in Betracht ziehen, Ihren Fokus auf IT-Sicherheit als Spezialisierung zu verlagern. Wir werden in dieser Vorlesung nicht tiefer in die Sicherheit einsteigen, aber die Grundlagen, die Sie hier lernen, werden Ihnen in diesen fortgeschrittenen Kursen gute Dienste leisten.

3.3.3 Verfügbarkeitsgetriebene Architektur

Was ist Verfügbarkeit? Verfügbarkeit misst, wie oft Ihr System betriebsbereit und zugänglich ist. Sie wird typischerweise als Prozentsatz ausgedrückt — “99,9% Verfügbarkeit” (genannt “drei Neunen”) bedeutet, dass Ihr System maximal 8,76 Stunden pro Jahr ausfallen darf. “99,99%” (vier Neunen) erlaubt nur 52 Minuten Ausfallzeit pro Jahr.

Warum ist das architektonisch wichtig? Ein einzelner Server wird irgendwann ausfallen — Hardware geht kaputt, Netzwerke fallen aus, Software stürzt ab. Wenn Ihre Architektur auf einer einzelnen Instanz von etwas Kritischem basiert, bestimmt dieser Single Point of Failure Ihre maximale Verfügbarkeit.

Das Kernprinzip: Redundanz. Um hohe Verfügbarkeit zu erreichen, müssen Sie Ihr System so entwerfen, dass wenn eine Komponente ausfällt, eine andere übernehmen kann. Dies hat tiefgreifende architektonische Implikationen:

Verfügbarkeitsziel Max. Ausfallzeit/Jahr Architektonische Anforderungen
99% ("zwei Neunen") 3,65 Tage Basis-Monitoring, manuelle Wiederherstellung akzeptabel
99,9% ("drei Neunen") 8,76 Stunden Redundante Komponenten, automatisches Failover
99,99% ("vier Neunen") 52,6 Minuten Mehrere Rechenzentren, keine Single Points of Failure
99,999% ("fünf Neunen") 5,26 Minuten Aktiv-Aktiv-Replikation, geografische Verteilung

Beispiel: Datenbank-Failover-Muster

Betrachten wir ein gängiges Verfügbarkeitsmuster — Datenbankreplikation mit automatischem Failover:

Datenbankreplikation mit automatischem Failover
🖥️ Anwendung
Abfragen
🗄️ Primäre DB ✏️ lesen/schreiben
🗄️ Replikat 1 👁️ nur lesen
🗄️ Replikat 2 👁️ nur lesen
🔄 Synchrone Replikation
Primär (verarbeitet Schreibvorgänge)
Replikate (verarbeiten Lesevorgänge)
Echtzeit-Synchronisation

Der Denkprozess hinter diesem Design:

  1. Primär verarbeitet Schreibvorgänge: Alle Schreiboperationen gehen an eine Datenbank, um Konflikte zu vermeiden
  2. Replikate verarbeiten Lesevorgänge: Leseoperationen können an jedes Replikat gehen und verteilen die Last
  3. Synchrone Replikation: Änderungen werden in Echtzeit auf Replikate kopiert
  4. Failover-Fähigkeit: Wenn die Primäre ausfällt, kann ein Replikat befördert werden

So übersetzt sich das in Code:

# Availability pattern: Multiple instances with failover
class HighAvailabilityService:
    """
    A service that maintains connections to multiple database instances.

    Design decisions:
    - Primary database handles all writes (consistency)
    - Replicas handle reads (scalability + availability)
    - Automatic failover when instances become unreachable
    """

    def __init__(self, primary_db: str, replica_dbs: list[str]):
        self.primary = primary_db
        self.replicas = replica_dbs

    def read_data(self, query: str):
        """
        Read from any available database.

        Why iterate through all databases?
        - If primary is slow or down, replicas can serve reads
        - If one replica fails, we try the next
        - Only raise error if ALL instances are unreachable

        This is called the "failover" pattern.
        """
        databases = [self.primary] + self.replicas

        for db in databases:
            try:
                return self._execute_query(db, query)
            except ConnectionError:
                # Log the failure, but don't crash—try next database
                self._log_failure(db)
                continue

        # Only fail if we've exhausted all options
        raise ServiceUnavailableError("All databases unreachable")

    def write_data(self, query: str):
        """
        Write only to primary database.

        Why not write to replicas?
        - Avoids conflicting writes (two users update same record)
        - Replication handles propagating changes
        - Simpler consistency model
        """
        return self._execute_query(self.primary, query)

    def health_check(self) -> dict:
        """
        Report which instances are healthy.

        Why expose this?
        - Load balancers use it to route traffic
        - Monitoring systems use it for alerts
        - Operations team uses it for debugging
        """
        return {
            "primary": self._is_healthy(self.primary),
            "replicas": [self._is_healthy(r) for r in self.replicas],
            "overall": self._is_healthy(self.primary) or any(
                self._is_healthy(r) for r in self.replicas
            ),
        }

Praxisbeispiel: Netflix

Netflix benötigt extrem hohe Verfügbarkeit — Millionen von Benutzern streamen gleichzeitig. Ihr internes Verfügbarkeitsziel ist 99,99% (nur 52 Minuten Ausfallzeit pro Jahr). Ihr architektonischer Ansatz umfasst:

Weiterführende Lektüre: Der Netflix Technology Blog dokumentiert ihre Engineering-Praktiken im Detail. Ihre Chaos Engineering getaggten Posts sind besonders wertvoll für das Verständnis von Resilienztests.

Trade-off: Hochverfügbarkeitsarchitekturen sind teuer. Jedes Replikat kostet Geld, und die Komplexität der Failover-Verwaltung erhöht den Entwicklungs- und Betriebsaufwand. Investieren Sie nur dort in hohe Verfügbarkeit, wo das Geschäft es wirklich erfordert.

3.3.4 Wartbarkeitsgetriebene Architektur

Was ist Wartbarkeit? Wartbarkeit misst, wie einfach Ihr System modifiziert werden kann — um Bugs zu beheben, Funktionen hinzuzufügen oder sich an veränderte Anforderungen anzupassen. Anders als Performance (die Sie in Millisekunden messen können) oder Verfügbarkeit (die Sie in Prozent der Betriebszeit messen können), ist Wartbarkeit schwieriger zu quantifizieren, aber kritisch wichtig für langlebige Software.

Warum ist das architektonisch relevant? Studien zeigen konsistent, dass 80% der Softwarekosten auf Wartung entfallen, nicht auf die initiale Entwicklung. Ein System, das schnell zu bauen, aber schwer zu ändern ist, wird mit der Zeit zunehmend teurer. Für Studentenprojekte, die an zukünftige Jahrgänge übergeben werden, ist Wartbarkeit wohl die wichtigste Qualität.

Das Kernprinzip: Separation of Concerns (Trennung der Verantwortlichkeiten). Eine wartbare Architektur isoliert verschiedene Verantwortlichkeiten, sodass:

  1. Änderungen lokalisiert sind: Das Modifizieren einer Funktion erfordert nicht das Anfassen von unzusammenhängendem Code
  2. Komponenten verständlich sind: Jedes Teil ist klein genug, um es vollständig zu verstehen
  3. Testen unkompliziert ist: Sie können jede Komponente isoliert testen
  4. Teams unabhängig arbeiten können: Verschiedene Entwickler können an verschiedenen Komponenten arbeiten, ohne Konflikte

Anzeichen für schlechte Wartbarkeit:

# ❌ BAD: Everything mixed together (the "God Class" anti-pattern)
class ProfileManager:
    def load_and_validate_and_display_and_save(self, path: str):
        # 500 lines mixing file I/O, validation, visualization, database access
        # Change to database? Touch this class.
        # Change to chart style? Touch this class.
        # Fix validation bug? Touch this class.
        # Every change risks breaking something unrelated.
        ...

Anzeichen für gute Wartbarkeit:

Die Schlüsselerkenntnis ist das Single Responsibility Principle (SRP): Jede Komponente sollte genau einen Grund zur Änderung haben. Wenden wir dies auf den Road Profile Viewer an:

# ✓ GOOD: Separated responsibilities

# ──────────────────────────────────────────────────────────────────
# FILE: profiles/repository.py
# RESPONSIBILITY: Data access (how profiles are stored/retrieved)
# CHANGES WHEN: Database technology changes, file format changes
# ──────────────────────────────────────────────────────────────────
class ProfileRepository:
    """
    Only responsibility: Data access for profiles.

    This class knows HOW to store and retrieve profiles,
    but knows nothing about validation rules or visualization.
    """

    def __init__(self, database_path: str):
        self.database_path = database_path

    def get(self, profile_id: str) -> Profile:
        """Load a profile from storage."""
        # Implementation details hidden here
        # Could be SQLite, PostgreSQL, JSON files—callers don't care
        ...

    def save(self, profile: Profile) -> None:
        """Persist a profile to storage."""
        ...

    def list_all(self) -> list[Profile]:
        """List all available profiles."""
        ...


# ──────────────────────────────────────────────────────────────────
# FILE: profiles/service.py
# RESPONSIBILITY: Business logic (rules and workflows)
# CHANGES WHEN: Business rules change, validation requirements change
# ──────────────────────────────────────────────────────────────────
class ProfileService:
    """
    Only responsibility: Business logic for profiles.

    This class knows WHAT operations are valid,
    but delegates HOW to store data to the repository.
    """

    def __init__(self, repository: ProfileRepository):
        # Dependency injection: receive repository, don't create it
        # This makes testing easy—pass a mock repository in tests
        self.repository = repository

    def validate_and_save(self, profile: Profile) -> None:
        """
        Validate a profile according to business rules, then save.

        Why separate from repository.save()?
        - Validation rules are business logic, not data access
        - Repository shouldn't know about business rules
        - We can change validation without touching storage code
        """
        self._validate_measurements(profile)
        self._validate_metadata(profile)
        self.repository.save(profile)

    def _validate_measurements(self, profile: Profile) -> None:
        """Check that measurements are physically plausible."""
        if profile.length_km < 0:
            raise ValidationError("Profile length cannot be negative")
        if profile.max_elevation > 9000:  # Higher than Everest
            raise ValidationError("Elevation seems implausible")

    def _validate_metadata(self, profile: Profile) -> None:
        """Check that required metadata is present."""
        if not profile.name:
            raise ValidationError("Profile must have a name")


# ──────────────────────────────────────────────────────────────────
# FILE: profiles/views.py
# RESPONSIBILITY: Visualization (how data is displayed)
# CHANGES WHEN: UI requirements change, chart library changes
# ──────────────────────────────────────────────────────────────────
def create_profile_chart(profile: Profile) -> Figure:
    """
    Only responsibility: Create a visual representation of a profile.

    This function knows HOW to create charts,
    but knows nothing about validation or storage.
    """
    fig, ax = plt.subplots()
    ax.plot(profile.distances, profile.elevations)
    ax.set_xlabel("Distance (km)")
    ax.set_ylabel("Elevation (m)")
    ax.set_title(profile.name)
    return fig


def create_gradient_chart(profile: Profile) -> Figure:
    """Create a chart showing road gradient (slope)."""
    # Different visualization, same responsibility category
    ...

Warum diese Struktur die Wartbarkeit verbessert:

Änderungsanfrage Ohne Trennung Mit Trennung
"Von SQLite auf PostgreSQL wechseln" Durch 2000-Zeilen-Klasse suchen, Risiko die Validierung zu brechen Nur repository.py anpassen
"Höhenlimit-Validierung hinzufügen" Durch 2000-Zeilen-Klasse suchen, Risiko die Diagramme zu brechen Nur service.py anpassen
"Diagrammfarben an Marke anpassen" Durch 2000-Zeilen-Klasse suchen, Risiko die Datenbank zu brechen Nur views.py anpassen
"Unit Tests für Validierung schreiben" Datenbank- und UI-Mocks einrichten müssen ProfileService mit Mock-Repository testen

Dependency Injection: Der Schlüssel zur Testbarkeit

Beachten Sie, wie ProfileService sein repository im Konstruktor erhält, anstatt es intern zu erstellen:

# ❌ Hard to test: Service creates its own dependencies
class ProfileService:
    def __init__(self):
        self.repository = ProfileRepository("production.db")  # Hardcoded!

# ✓ Easy to test: Dependencies are injected
class ProfileService:
    def __init__(self, repository: ProfileRepository):
        self.repository = repository  # Caller decides which repository

# In production:
service = ProfileService(ProfileRepository("production.db"))

# In tests:
mock_repo = MockProfileRepository()  # Fake that doesn't touch real database
service = ProfileService(mock_repo)
service.validate_and_save(test_profile)
assert mock_repo.save_was_called_with(test_profile)

Trade-off: Wartbare Architekturen haben mehr Dateien, mehr Klassen und mehr Indirektion. Ein einfaches Skript wird zu einem strukturierten Projekt. Dieser Overhead lohnt sich nicht für Wegwerf-Code, aber für jede Software, die über ihre initiale Entwicklung hinaus lebt, zahlt sich die Investition aus.

Für den Road Profile Viewer: Als Studentenprojekt, das zukünftige Jahrgänge erben werden, priorisieren Sie Wartbarkeit. Ihre Nachfolger werden es Ihnen danken, wenn sie den Code verstehen und modifizieren können, ohne Angst haben zu müssen, unzusammenhängende Funktionen zu brechen.

3.4 Trade-offs: Man kann nicht alles optimieren

Hier ist die harte Wahrheit: Architekturziele stehen oft in Konflikt.

Ziel A Steht in Konflikt mit Warum
Performance Wartbarkeit Große Komponenten sind schnell, aber schwer zu ändern; kleine Komponenten sind flexibel, aber verursachen Kommunikations-Overhead
Sicherheit Benutzerfreundlichkeit Mehr Sicherheitsschichten bedeuten mehr Reibung für legitime Benutzer
Verfügbarkeit Kosten Redundante Systeme sind teuer zu bauen und zu betreiben
Flexibilität Einfachheit Konfigurierbare Systeme haben mehr Codepfade und potenzielle Bugs

Wie man mit Trade-offs umgeht:

  1. Priorisieren: Entscheiden Sie, welche nicht-funktionalen Anforderungen für Ihr System am wichtigsten sind
  2. Partitionieren: Verwenden Sie verschiedene Architekturen für verschiedene Teile (performance-kritischer Kern + wartbare Plugins)
  3. Dokumentieren: Halten Sie fest, warum Sie jeden Trade-off gemacht haben, für zukünftige Entwickler

Für den Road Profile Viewer: Wartbarkeit und Einfachheit sollten Prioritäten sein. Performance-Optimierung kann warten, bis Sie tatsächlich 1.000 Benutzer haben.


4. Architektursichten: Mehrere Perspektiven auf ein System

4.1 Warum ein Diagramm nie ausreicht

Ein einzelnes Diagramm kann nicht alle Aspekte der Architektur eines Systems erfassen. Betrachten Sie, was verschiedene Stakeholder wissen müssen:

Jede dieser Fragen erfordert eine andere Sicht auf dasselbe System.

“Es ist unmöglich, alle relevanten Informationen über die Architektur eines Systems in einem einzigen Diagramm darzustellen.” — Sommerville, Software Engineering

4.2 Das 4+1-Sichtenmodell

Philippe Krutchen schlug 1995 das 4+1-Sichtenmodell vor, das zur Standardmethode zur Beschreibung von Softwarearchitektur geworden ist. Es besteht aus vier fundamentalen Sichten, verbunden durch Szenarien (Use Cases):

🎯 Szenarien Use Cases (+1)
Objekte & Klassen Laufzeit-Prozesse Code-Organisation Hardware-Deployment
🧠 Logische Sicht Zentrale Abstraktionen als Objekte und Klassen
Prozess-Sicht Laufzeit-Interaktionen und Prozesse
🖥️ Physische Sicht Hardware und Deployment-Topologie
📦 Entwicklungs-Sicht Code-Module und Pakete
Sicht Zeigt Zielgruppe Zentrale Frage
Logische Sicht Zentrale Abstraktionen als Objekte oder Objektklassen Entwickler, Domänenexperten "Was sind die Hauptkonzepte und wie hängen sie zusammen?"
Prozess-Sicht Laufzeit-Komposition interagierender Prozesse Performance-Ingenieure, Integratoren "Was läuft wann, und wie interagieren Komponenten zur Laufzeit?"
Entwicklungs-Sicht Wie Software für die Entwicklung zerlegt wird Programmierer, Projektmanager "Wie ist der Code in Module und Pakete organisiert?"
Physische Sicht System-Hardware und Verteilung von Komponenten System-Ingenieure, Betrieb "Welche Hardware führt welche Software aus?"
Szenarien (+1) Use Cases, die die Architektur durchspielen Alle Stakeholder "Wie fließt eine Benutzeranfrage durch alle Schichten?"

4.2.1 Logische Sicht

Die logische Sicht zeigt die zentralen Abstraktionen des Systems — die Konzepte, die die Domäne ausmachen.

Logische Sicht des Road Profile Viewer:

classDiagram
    class RoadProfile {
        +name: str
        +x_coordinates: list
        +y_coordinates: list
        +metadata: dict
    }

    class ProfileRepository {
        +get(id): RoadProfile
        +save(profile): void
        +list_all(): list
    }

    class GeometryCalculator {
        +find_intersection(ray, surface)
        +calculate_angle(point1, point2)
    }

    class ChartBuilder {
        +build_profile_chart(profile): Figure
        +build_comparison_chart(profiles): Figure
    }

    ProfileRepository --> RoadProfile
    ChartBuilder --> RoadProfile
    GeometryCalculator --> RoadProfile

Diese Sicht bezieht sich direkt auf Anforderungen: “Das System muss Straßenprofile verwalten, geometrische Berechnungen durchführen und Diagramme anzeigen.”

4.2.2 Prozess-Sicht

Die Prozess-Sicht zeigt, was zur Laufzeit geschieht — Prozesse, Threads und ihre Interaktionen.

Prozess-Sicht des Road Profile Viewer:

sequenceDiagram
    participant User
    participant DashApp
    participant Callbacks
    participant Database

    User->>DashApp: Select profile from dropdown
    DashApp->>Callbacks: Trigger update callback
    Callbacks->>Database: Query profile data
    Database-->>Callbacks: Return profile
    Callbacks->>Callbacks: Generate chart
    Callbacks-->>DashApp: Return updated figure
    DashApp-->>User: Display chart

Diese Sicht hilft bei der Performance-Analyse: “Jede Benutzerinteraktion löst eine Datenbankabfrage aus — ist das ein Engpass?”

4.2.3 Entwicklungs-Sicht

Die Entwicklungs-Sicht zeigt, wie Code in Pakete und Module organisiert ist.

Entwicklungs-Sicht des Road Profile Viewer:

road-profile-viewer/
├── src/
│   ├── __init__.py
│   ├── models/           # Domain models
│   │   ├── __init__.py
│   │   └── profile.py    # RoadProfile class
│   ├── repositories/     # Data access
│   │   ├── __init__.py
│   │   └── profile_repository.py
│   ├── services/         # Business logic
│   │   ├── __init__.py
│   │   ├── geometry.py   # Calculations
│   │   └── analysis.py   # Profile analysis
│   ├── presentation/     # UI components
│   │   ├── __init__.py
│   │   ├── app.py        # Dash application
│   │   └── charts.py     # Chart builders
│   └── main.py           # Entry point
├── tests/
│   ├── test_models.py
│   ├── test_services.py
│   └── test_repositories.py
└── pyproject.toml

Diese Sicht hilft bei der Koordination der Entwicklung: “Anna arbeitet an services/, Ben arbeitet an presentation/.”

4.2.4 Physische Sicht

Die physische Sicht zeigt Hardware und Deployment.

Road Profile Viewer — Aktuell (Einfach):

Entwickler-Laptop
Python-Prozess
Dash App :8050
SQLite Datei

Road Profile Viewer — Skaliert (Zukunft):

🌐
Webbrowser
Benutzer 1
🌐
Webbrowser
Benutzer 2
⚖️
Load Balancer
🖥️
App Server 1
Dash / Flask
🖥️
App Server 2
Dash / Flask
🐘
PostgreSQL-Datenbank
Managed Service

4.2.5 Szenarien (Das +1)

Szenarien sind Use Cases, die die Architektur durchspielen. Sie verbinden alle vier Sichten, indem sie eine Benutzeraktion durch das System verfolgen.

Szenario: Benutzer lädt ein neues Straßenprofil hoch

  1. Physisch: Anfrage kommt beim Load Balancer an, wird zu App Server 1 geleitet
  2. Prozess: Upload-Handler validiert Datei, parst JSON, ruft ProfileService auf
  3. Logisch: ProfileService erstellt RoadProfile-Objekt, validiert mit Pydantic
  4. Entwicklung: Code in services/profile_service.py ruft repositories/profile_repository.py auf
  5. Physisch: Datenbank-Schreibvorgang geht an PostgreSQL-Server
  6. Prozess: Antwort fließt durch die Schichten zurück zum Browser des Benutzers

4.3 Anwendung der Sichten auf den Road Profile Viewer

Sie brauchen keine formalen UML-Diagramme für ein Studentenprojekt. Einfache Skizzen und Markdown-Beschreibungen funktionieren gut:

Schnelle Architekturdokumentation (Markdown):

ROAD PROFILE VIEWER ARCHITEKTUR
================================

LOGISCHE SICHT
- RoadProfile: Domänenentität mit Koordinaten und Metadaten
- ProfileRepository: Behandelt Datenbankoperationen
- GeometryCalculator: Strahl-Oberflächen-Schnittmathematik
- DashApp: Interaktive Web-Visualisierung

ENTWICKLUNGSSICHT
- src/models/ - Pydantic-Datenmodelle
- src/services/ - Geschäftslogik
- src/presentation/ - Dash UI-Komponenten

PROZESSSICHT
- Single-threaded Dash-App
- Callbacks ausgelöst durch Benutzerinteraktionen
- Synchrone Datenbankabfragen

PHYSISCHE SICHT (Aktuell)
- Läuft auf einer einzelnen Maschine
- SQLite-Dateidatenbank
- Entwicklungsserver auf Port 8050

SCHLÜSSELSZENARIO: Profil anzeigen
Benutzer wählt Profil → Dropdown-Callback →
Datenbankabfrage → Chart-Generierung → UI-Update

4.3.1 Ein Blick auf die formale UML-Notation

Während einfache Skizzen für Studentenprojekte gut funktionieren, verwendet professionelles Software Engineering UML (Unified Modeling Language) — eine standardisierte visuelle Notation für Softwarearchitektur und -design.

UML-Diagramme bieten eine gemeinsame Sprache, die Architekten, Entwickler und Stakeholder weltweit verstehen. Jeder Diagrammtyp entspricht bestimmten architektonischen Sichten.

So würden die 4+1-Sichten mit UML-artigen Diagrammen aussehen:

Logische Sicht — Klassendiagramm:

classDiagram
    class RoadProfile {
        +str id
        +str name
        +list~float~ x_coordinates
        +list~float~ y_coordinates
        +get_elevation_at(x: float) float
        +max_slope() float
    }

    class ProfileRepository {
        -str db_path
        +get(profile_id: str) RoadProfile
        +save(profile: RoadProfile) RoadProfile
        +list_all() list~RoadProfile~
    }

    class GeometryCalculator {
        +calculate_intersection(ray: Ray, surface: Surface) Point
        +interpolate(x: float, profile: RoadProfile) float
    }

    ProfileRepository --> RoadProfile : creates/retrieves
    GeometryCalculator --> RoadProfile : uses

Prozesssicht — Sequenzdiagramm:

sequenceDiagram
    participant U as User
    participant V as Dash UI
    participant C as Callback
    participant S as ProfileService
    participant R as Repository
    participant DB as SQLite

    U->>V: Select profile from dropdown
    V->>C: Trigger callback(profile_id)
    C->>S: get_profile(profile_id)
    S->>R: get(profile_id)
    R->>DB: SELECT * FROM profiles
    DB-->>R: Row data
    R-->>S: RoadProfile object
    S-->>C: Profile with analysis
    C-->>V: Plotly Figure
    V-->>U: Display chart

Entwicklungssicht — Paketdiagramm:

graph LR
    subgraph presentation["📦 presentation"]
        app[app.py]
        callbacks[callbacks.py]
        charts[charts.py]
    end

    subgraph domain["📦 domain"]
        models[models.py]
        services[services.py]
    end

    subgraph infrastructure["📦 infrastructure"]
        repositories[repositories.py]
        database[database.py]
    end

    presentation --> domain
    domain --> infrastructure

    style presentation fill:#dbeafe,stroke:#3b82f6
    style domain fill:#dcfce7,stroke:#22c55e
    style infrastructure fill:#fef3c7,stroke:#f59e0b

Physische Sicht — Deployment-Diagramm:

graph TB
    subgraph client["🖥️ Client Machine"]
        browser[Web Browser]
    end

    subgraph server["💻 Developer Laptop"]
        subgraph python["🐍 Python Process"]
            dash[Dash App :8050]
            sqlite[(SQLite DB)]
        end
    end

    browser -->|HTTP| dash
    dash -->|File I/O| sqlite

    style client fill:#dbeafe,stroke:#3b82f6
    style server fill:#1e293b,stroke:#475569,color:#fff
    style python fill:#166534,stroke:#22c55e,color:#fff
UML-Diagrammtyp 4+1-Sicht Zeigt
Klassendiagramm Logische Sicht Klassen, Attribute, Methoden, Beziehungen
Sequenzdiagramm Prozesssicht Objektinteraktionen über die Zeit
Paketdiagramm Entwicklungssicht Codeorganisation und Abhängigkeiten
Deployment-Diagramm Physische Sicht Hardware-Knoten und Softwareverteilung
Use-Case-Diagramm Szenarien (+1) Akteur-System-Interaktionen

Ausblick: UML-Diagramme und formale Modellierungstechniken werden in späteren Kursen zu Software Design und Systemmodellierung vertieft behandelt. Für jetzt gibt Ihnen das Verständnis, dass diese standardisierten Notationen existieren — und wie sie sich auf architektonische Sichten abbilden — eine Grundlage zum Aufbauen.

Für den Road Profile Viewer: Einfache Markdown-Dokumentation und Mermaid-Diagramme sind ausreichend. Wenn Ihre Projekte an Komplexität und Teamgröße wachsen, wird formales UML wertvoller für Kommunikation und Dokumentation.


5. Zusammenfassung

Konzept Kernaussage Road Profile Viewer Anwendung
Softwarearchitektur Die grundlegende Organisation eines Systems als kommunizierende Komponenten Definiert, wie Dash UI, Geschäftslogik und Datenbank interagieren
Architekturentscheidungen Kreativer Prozess, getrieben von nicht-funktionalen Anforderungen Wartbarkeit über Leistung für Studentenprojekt priorisieren
Nicht-funktionale Anforderungen Performance, Sicherheit, Verfügbarkeit, Wartbarkeit prägen die Architektur Fokus auf Wartbarkeit und Einfachheit zum Lernen
4+1-Sichten Logisch, Prozess, Entwicklung, Physisch + Szenarien Jede Sicht im README für Teamkoordination dokumentieren
Trade-offs Kann nicht alle Qualitäten optimieren; muss priorisieren Einfach und wartbar statt skalierbar und komplex

5.1 Wichtige Erkenntnisse

  1. Architektur ist von Anfang an wichtig — auch kleine Projekte profitieren von absichtlicher Struktur
  2. Nicht-funktionale Anforderungen prägen die Architektur — Performance, Sicherheit und Wartbarkeit treiben Designentscheidungen
  3. Mehrere Sichten sind notwendig — kein einzelnes Diagramm erfasst die vollständige Architektur (4+1-Modell)
  4. Trade-offs sind unvermeidlich — Sie können nicht alle Qualitäten optimieren; priorisieren Sie basierend auf den Bedürfnissen Ihres Systems
  5. Architektur ermöglicht Kommunikation — Blockdiagramme und Sichten helfen Stakeholdern, das System zu verstehen

6. Reflexionsfragen

  1. Aktuelle Architektur: Wie würden Sie die aktuelle Architektur Ihres Road Profile Viewer Projekts beschreiben? Welche Sichten (logisch, Prozess, Entwicklung, physisch) können Sie identifizieren?

  2. Nicht-funktionale Prioritäten: Was sind die drei wichtigsten nicht-funktionalen Anforderungen für Ihren Road Profile Viewer? Wie beeinflussen diese Ihre architektonischen Entscheidungen?

  3. Trade-off-Szenario: Wenn Sie sich entscheiden müssten zwischen einem Road Profile Viewer, der einfacher zu warten ist, ODER einem, der schneller läuft, was würden Sie wählen? Warum?

  4. 4+1-Übung: Zeichnen Sie ein einfaches Diagramm für jede der vier Sichten Ihres Road Profile Viewers. Welche Erkenntnisse bringt dieser Multi-Sichten-Ansatz?


7. Was kommt als Nächstes

In Teil 1 haben Sie gelernt:

Demnächst: Teil 2 — Architekturmuster

In Teil 2: Architekturmuster werden wir erkunden:

Sie haben gelernt, warum Architektur wichtig ist. Jetzt lernen Sie, wie man bewährte Muster anwendet.

© 2026 Dominik Mueller   •  Powered by Soopr   •  Theme  Moonwalk