06 Softwarearchitektur Teil 1: Grundlagen und Architektursichten
January 2026 (10728 Words, 60 Minutes)
1. Einführung: Ihre Sprints sind schnell, aber kann Ihr System skalieren?
In Teil 1 und Teil 2 der agilen Entwicklung haben wir gelernt:
- Veränderungen durch kurze Iterationszyklen anzunehmen
- Alle 1-4 Wochen funktionierende Software zu liefern
- Arbeit mit Scrums Rollen, Events und Artefakten zu organisieren
- User Stories mit GitHub Issues und Projects zu verfolgen
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:
- Ihre Dash-App läuft auf einem einzelnen Laptop
- SQLite kann keine gleichzeitigen Schreibvorgänge von mehreren Standorten verarbeiten
- Die gesamte Anwendung ist ein Python-Prozess
- Es gibt keine Möglichkeit, einzelne Komponenten unabhängig zu skalieren
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:
- Wie werden Verantwortlichkeiten auf Komponenten verteilt?
- Wie kommunizieren Komponenten miteinander?
- Wie wird das System auf Hardware bereitgestellt?
- Was passiert, wenn eine Komponente ausfällt?
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:
- Wo werden Daten gespeichert?
- Wie werden sich Benutzer authentifizieren?
- Welche Programmiersprachen und Frameworks werden wir verwenden?
- Wie werden Komponenten kommunizieren?
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:
- Die Motivation und Ziele für architektonisches Design zu benennen — warum Architektur wichtig ist und was ein Architekturmodell ermöglicht
- Die Abhängigkeit zwischen Systemstruktur und nicht-funktionalen Anforderungen zu erklären — wie Performance, Sicherheit und Wartbarkeit architektonische Entscheidungen prägen
- 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:
- Systemtyp: Ein Echtzeit-Embedded-System hat andere Bedürfnisse als eine Webanwendung
- Erfahrung des Architekten: Wissen darüber, was in ähnlichen Systemen funktioniert hat (und was nicht)
- Spezifische Anforderungen: Die einzigartigen Einschränkungen und Ziele Ihres Projekts
“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:
Für den Road Profile Viewer, bedenken Sie:
- Vorlage: Web-Dashboard-Anwendungen haben gemeinsame Muster, denen wir folgen können
- Verteilung: Derzeit Einzelmaschine; könnte Datenbank und UI separat verteilen
- Muster: MVC für UI, Schichtenarchitektur für Trennung der Verantwortlichkeiten
- Struktur: Modulare Python-Pakete mit klaren Verantwortlichkeiten
- Zerlegung: Visualisierung, Datenzugriff und Geschäftslogik trennen
- Steuerung: Request-Response für Web, Callbacks für Dash-Interaktionen
- Nicht-funktional: Wartbarkeit priorisieren (Studentenprojekt) vor Performance
- Dokumentation: README mit Architekturdiagramm, Docstrings im Code
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:
- Kritische Operationen lokalisieren: Performance-sensitiven Code in so wenigen Komponenten wie möglich halten, um Kommunikations-Overhead zu minimieren
- Netzwerk-Hops minimieren: Jeder Netzwerkaufruf fügt Latenz hinzu (typischerweise 1-100ms); lokale Funktionsaufrufe sind Nanosekunden
- Daten und Berechnung zusammenlegen: Daten dort verarbeiten, wo sie liegen; keine großen Datensätze über Grenzen verschieben
- 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:
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?
- Dedizierte ECUs pro Funktion: Jede Steuereinheit übernimmt eine Verantwortung mit garantiertem Timing
- Parallele Sensorverarbeitung: LiDAR, Kameras und Radar werden gleichzeitig verarbeitet, nicht sequentiell
- GPU für Perzeption: Neuronale Netze zur Objekterkennung benötigen massive parallele Berechnung
- Hochfrequente Regelkreise: Lenkung und Bremsung laufen bei 1 kHz (1000 mal/Sekunde) für sanfte Kontrolle
- Deterministische Kommunikation: CAN-Bus oder Ethernet mit Time-Sensitive Networking (TSN) gewährleistet vorhersagbare Nachrichtenzustellung
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:
- Keine dynamische Speicherallokation:
malloc()hat unvorhersagbares Timing - Keine Garbage Collection: GC-Pausen würden Deadlines verpassen
- Direkter Hardwarezugriff: Keine Abstraktionsschichten, die Latenz hinzufügen
- Vorberechnete Lookup-Tabellen: Teure Berechnungen zur Laufzeit vermeiden
- Begrenzte Ausführungszeit: Jeder Codepfad muss innerhalb der Deadline abgeschlossen sein
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:
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:
Der Denkprozess hinter diesem Design:
- Primär verarbeitet Schreibvorgänge: Alle Schreiboperationen gehen an eine Datenbank, um Konflikte zu vermeiden
- Replikate verarbeiten Lesevorgänge: Leseoperationen können an jedes Replikat gehen und verteilen die Last
- Synchrone Replikation: Änderungen werden in Echtzeit auf Replikate kopiert
- 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:
-
Mehrere AWS-Regionen: Netflix stellt Services über US-East-1 (Virginia) und US-West-2 (Oregon) in einer Active-Active-Konfiguration bereit. Benutzer werden per Geo-DNS zur nächsten Region geleitet. Wenn eine Region einen Ausfall erlebt, kann der gesamte Traffic zur gesunden Region umgeleitet werden.
-
Chaos Engineering: Netflix war Pionier im Chaos Engineering mit Chaos Monkey — einem Tool, das zufällig Produktionsinstanzen beendet, um sicherzustellen, dass Ingenieure resiliente Services bauen. Sie verwenden auch Chaos Gorilla (tötet ganze Availability Zones) und Chaos Kong (simuliert vollständigen Regionsausfall). Wie Netflix erklärt: “Wir konnten unsere Teams um die Idee der Infrastruktur-Resilienz ausrichten, indem wir die durch Server-Neutralisierung verursachten Probleme isolierten und auf die Spitze trieben.”
-
Zustandslose Services: Kein einzelner Service hält kritischen Zustand; alles kann aus verteiltem Speicher rekonstruiert werden. Dies ermöglicht es jeder Instanz, jede Anfrage zu bearbeiten, was Failover nahtlos macht.
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:
- Änderungen lokalisiert sind: Das Modifizieren einer Funktion erfordert nicht das Anfassen von unzusammenhängendem Code
- Komponenten verständlich sind: Jedes Teil ist klein genug, um es vollständig zu verstehen
- Testen unkompliziert ist: Sie können jede Komponente isoliert testen
- 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:
- Priorisieren: Entscheiden Sie, welche nicht-funktionalen Anforderungen für Ihr System am wichtigsten sind
- Partitionieren: Verwenden Sie verschiedene Architekturen für verschiedene Teile (performance-kritischer Kern + wartbare Plugins)
- 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:
- Entwickler müssen wissen: Welche Klassen existieren? Welche Pakete enthalten sie?
- Operations-Ingenieure müssen wissen: Welche Server führen welche Services aus? Wie kommunizieren sie?
- Projektmanager müssen wissen: Welche Teams besitzen welche Komponenten?
- Sicherheitsprüfer müssen wissen: Wo fließen sensible Daten?
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):
| 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):
Road Profile Viewer — Skaliert (Zukunft):
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
- Physisch: Anfrage kommt beim Load Balancer an, wird zu App Server 1 geleitet
- Prozess: Upload-Handler validiert Datei, parst JSON, ruft ProfileService auf
- Logisch: ProfileService erstellt RoadProfile-Objekt, validiert mit Pydantic
- Entwicklung: Code in
services/profile_service.pyruftrepositories/profile_repository.pyauf - Physisch: Datenbank-Schreibvorgang geht an PostgreSQL-Server
- 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
- Architektur ist von Anfang an wichtig — auch kleine Projekte profitieren von absichtlicher Struktur
- Nicht-funktionale Anforderungen prägen die Architektur — Performance, Sicherheit und Wartbarkeit treiben Designentscheidungen
- Mehrere Sichten sind notwendig — kein einzelnes Diagramm erfasst die vollständige Architektur (4+1-Modell)
- Trade-offs sind unvermeidlich — Sie können nicht alle Qualitäten optimieren; priorisieren Sie basierend auf den Bedürfnissen Ihres Systems
- Architektur ermöglicht Kommunikation — Blockdiagramme und Sichten helfen Stakeholdern, das System zu verstehen
6. Reflexionsfragen
-
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?
-
Nicht-funktionale Prioritäten: Was sind die drei wichtigsten nicht-funktionalen Anforderungen für Ihren Road Profile Viewer? Wie beeinflussen diese Ihre architektonischen Entscheidungen?
-
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+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:
- Warum Architektur wichtig ist — die Brücke zwischen agiler Lieferung und nachhaltigen Systemen
- Architektonische Designentscheidungen — wie nicht-funktionale Anforderungen die Systemstruktur formen
- Das 4+1-Sichtenmodell — Systeme durch mehrere Perspektiven verstehen
Demnächst: Teil 2 — Architekturmuster
In Teil 2: Architekturmuster werden wir erkunden:
- MVC (Model-View-Controller) — Trennung von Verantwortlichkeiten in interaktiven Anwendungen
- Schichtenarchitektur — Code in Präsentations-, Geschäfts- und Datenschichten organisieren
- Verteilte Muster — Client-Server, Microservices und wann man sie verwendet
- Architektur auf den Road Profile Viewer anwenden — Ihre Projektstruktur weiterentwickeln
Sie haben gelernt, warum Architektur wichtig ist. Jetzt lernen Sie, wie man bewährte Muster anwendet.