| Komponente | Verantwortlichkeit | Road Profile Viewer |
|---|---|---|
| Model | Daten und Geschäftslogik; weiß nichts über UI | RoadProfile-Klasse, GeometryCalculator |
| View | Zeigt Daten an; empfängt Benutzereingaben | Dash-Layout, Plotly-Charts |
| Controller | Verarbeitet Benutzereingaben; aktualisiert Model und View | Dash-Callbacks |
Vorteile:
Kompromisse:
Hinweis: Django verwendet MTV (Model-Template-View) Terminologie, nicht MVC. Djangos "View" fungiert als Controller!
# models.py — Model (wie bei MVC)
class RoadProfile(models.Model):
name = models.CharField(max_length=100)
data = models.JSONField()
# views.py — Django "View" = MVC Controller (verarbeitet Anfragen)
def profile_detail(request, profile_id):
profile = RoadProfile.objects.get(id=profile_id)
return render(request, 'profile.html', {'profile': profile})
# templates/profile.html — Django "Template" = MVC View (Darstellung)
<h1>{{ profile.name }}</h1>
# models.py (Model)
@dataclass
class RoadProfile:
id: str
name: str
coordinates: list[tuple[float, float]]
# routes.py (Controller)
@app.route('/profiles/<profile_id>')
def show_profile(profile_id):
profile = ProfileRepository.get(profile_id)
return render_template('profile.html', profile=profile)
# templates/profile.html (View) - Jinja2-Template
2 Minuten: Denken Sie an Webanwendungen, die Sie genutzt haben.
2 Minuten: Diskutieren Sie mit einem Nachbarn.
Teilen: Welche Muster erkennen Sie?
Diskussionsfragen:
Model (models/profile.py):
class RoadProfile(BaseModel):
"""Domain-Model - weiß nichts über UI oder Speicherung."""
id: str
name: str
x_coordinates: list[float]
y_coordinates: list[float]
def max_slope(self) -> float:
"""Geschäftslogik lebt im Model."""
...
View (presentation/charts.py):
def create_profile_figure(profile: RoadProfile) -> go.Figure:
"""Reine Visualisierung - keine Geschäftslogik."""
return go.Figure(
data=go.Scatter(
x=profile.x_coordinates,
y=profile.y_coordinates,
mode='lines', name=profile.name
),
layout=go.Layout(title=f"Straßenprofil: {profile.name}")
)
Controller (presentation/callbacks.py):
@callback(
Output('profile-chart', 'figure'),
Input('profile-dropdown', 'value')
)
def update_chart(profile_id: str):
"""Controller: koordiniert Model und View."""
if not profile_id:
return go.Figure() # Leere Figur
profile = repository.get(profile_id) # Model holen
return create_profile_figure(profile) # View erstellen
Mit sauberer MVC-Trennung:
RoadProfile kann ohne Dash getestet werdenDas Model weiß nichts über die View.
Die View weiß nichts über den Controller.
Alle sprechen mit dem Model.
Schichtenarchitektur organisiert Code in horizontale Schichten, wobei jede Schicht nur von der darunterliegenden Schicht abhängt.
┌─────────────────────────────────┐
│ Präsentationsschicht │ ← Benutzeroberfläche
└─────────────────────────────────┘
│ hängt ab von
▼
┌─────────────────────────────────┐
│ Geschäftsschicht │ ← Domänenlogik
└─────────────────────────────────┘
│ hängt ab von
▼
┌─────────────────────────────────┐
│ Datenzugriffsschicht │ ← Datenbankoperationen
└─────────────────────────────────┘
Die drei goldenen Regeln:
Nur Abwärts-Abhängigkeiten: Präsentation → Geschäft → Daten
Keine Schichten überspringen: Präsentation sollte NICHT direkt Daten aufrufen
Schichten sind kohäsiv: Aller UI-Code in Präsentation, alle Domänenlogik in Geschäft
Warum? Änderungen in einer Schicht kaskadieren nicht durch das gesamte System.
| Schicht | Verantwortlichkeit | Typische Komponenten |
|---|---|---|
| Präsentation | Benutzerinteraktion, Daten anzeigen | Webseiten, API-Endpunkte, CLI |
| Geschäft | Domänenlogik, Validierung, Berechnungen | Services, Domänenmodelle, Validatoren |
| Datenzugriff | Daten persistieren und abrufen | Repositories, ORM-Models, Cache |
Netflix demonstriert Schichtenprinzipien im großen Maßstab:
┌─────────────────────────────────────────────────────────┐
│ PRÄSENTATION: Web-UI, Mobile Apps, TV Apps, API Gateway│
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ GESCHÄFT: User Service, Content Service, Recommendation│
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ DATEN: Cassandra, MySQL, ElasticSearch, S3 │
└─────────────────────────────────────────────────────────┘
| Komponente | Beschreibung |
|---|---|
| Web UI | React Single Page Application |
| Mobile Apps | iOS & Android native Apps |
| TV Apps | Smart TVs, Roku, PlayStation |
| API Gateway (Zuul) | Leitet Anfragen an Services |
Jeder Präsentations-Client kann unabhängig aktualisiert werden!
| Service | Verantwortlichkeit |
|---|---|
| User Service | Authentifizierung & Profile |
| Content Service | Filme & Serien Metadaten |
| Recommendation | ML-gestützte Vorschläge |
| Playback Service | Streaming-Logik |
Jeder Service kann unabhängig skaliert werden basierend auf Bedarf!
| Datenbank | Anwendungsfall |
|---|---|
| Cassandra | Benutzerdaten & Wiedergabeverlauf |
| MySQL | Abrechnung & Konten |
| ElasticSearch | Suchindizierung |
| S3 | Videodateien (Petabytes!) |
Wichtige Erkenntnis: Verschiedene Datenspeicher für verschiedene Bedürfnisse!
Unabhängige Skalierung:
Technologie-Flexibilität:
Jede Schicht kann sich unabhängig weiterentwickeln.
@app.callback(...)
def update_chart(profile_id):
if not profile_id:
return {} # UI-Logik
conn = sqlite3.connect('profiles.db') # Datenzugriff
cursor = conn.execute('SELECT * FROM profiles WHERE id = ?', (profile_id,))
row = cursor.fetchone()
x_coords = json.loads(row[1]) # Geschäftslogik
y_coords = json.loads(row[2])
max_slope = calculate_max_slope(x_coords, y_coords)
return create_figure(x_coords, y_coords, max_slope) # Wieder UI
Problem: Alle Schichten in einer Funktion vermischt!
data_access/repositories.py:
class ProfileRepository:
def __init__(self, db_path: str = 'profiles.db'):
self.db_path = db_path
def get(self, profile_id: str) -> Profile | None:
conn = sqlite3.connect(self.db_path)
cursor = conn.execute(
'SELECT * FROM profiles WHERE id = ?', (profile_id,)
)
row = cursor.fetchone()
return Profile.from_row(row) if row else None
business/services.py:
class ProfileService:
def __init__(self, repository: ProfileRepository):
self.repository = repository
def get_profile_with_analysis(self, profile_id: str) -> dict:
profile = self.repository.get(profile_id)
if not profile:
raise ProfileNotFoundError(profile_id)
return {
'profile': profile,
'max_slope': self._calculate_max_slope(profile),
}
presentation/callbacks.py:
service = ProfileService(ProfileRepository())
@app.callback(...)
def update_chart(profile_id):
if not profile_id:
return empty_figure()
try:
data = service.get_profile_with_analysis(profile_id)
return create_figure(data)
except ProfileNotFoundError:
return error_figure("Profil nicht gefunden")
Sauber! Jede Schicht hat eine Verantwortlichkeit.
Szenario: Profil-Berechnungen sind langsam. Sie möchten Ergebnisse cachen.
Diskutieren Sie:
2 Minuten nachdenken, 2 Minuten zu zweit, dann teilen!
┌──────────────┐
│ Client 1 │ ──────┐
│ (Webbrowser) │ │
└──────────────┘ │
▼
┌──────────────┐ ┌──────────┐ ┌──────────┐
│ Client 2 │ → │ Server │ → │ Datenbank│
│ (Mobile App) │ │ (Web) │ └──────────┘
└──────────────┘ └──────────┘
▲
┌──────────────┐ │
│ Client 3 │ ──────┘
│(Desktop-App) │
└──────────────┘
| Komponente | Verantwortlichkeit |
|---|---|
| Clients | Anfragen initiieren, Ergebnisse anzeigen, Benutzereingaben verarbeiten |
| Server | Anfragen verarbeiten, Daten verwalten, Geschäftsregeln durchsetzen |
| Protokoll | Meist HTTP/HTTPS für Webanwendungen |
Road Profile Viewer ist bereits Client-Server:
Microservices zerlegen ein System in kleine, unabhängig deploybare Services:
┌─────────────────┐
│ API Gateway │
└────────┬────────┘
┌───────────┼───────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ User │ │ Profil │ │ Analyse │
│ Service │ │ Service │ │ Service │
└────┬─────┘ └────┬─────┘ └────┬─────┘
▼ ▼ ▼
[User DB] [Profil DB] [Cache]
Monolith:
Microservices:
2002 verfügte Jeff Bezos:
"Alle Teams müssen ihre Funktionalität über Service-Schnittstellen bereitstellen."
Die "Zwei-Pizza-Team"-Regel: Teams klein genug, um mit zwei Pizzen satt zu werden.
Ergebnis: Amazon Web Services — ursprünglich interne Infrastruktur, jetzt ein 80+ Milliarden Dollar Geschäft.
JA, verwenden wenn:
NICHT verwenden wenn:
Road Profile Viewer: Bleibt beim Monolith! Ihr habt 3 Entwickler und eine einfache Domäne.
Szenario: Euer Road Profile Viewer Team wächst von 3 auf 30 Entwickler. Ihr habt jetzt 5 Feature-Teams, die an verschiedenen Teilen arbeiten.
Welche Architektur würden Sie empfehlen?
A) Monolith behalten, nur Code besser organisieren
B) In Microservices aufteilen
C) Mit "modularem Monolith" starten und später aufteilen
Cloud-Plattformen bieten Bausteine für verteilte Architekturen:
Users → CloudFront (CDN) → Load Balancer → ECS/Fargate → RDS
(Edge Cache) (Routing) (Container) (Datenbank)
Für Road Profile Viewer (Zukunft):
Ihr braucht keine AWS-Komplexität für ein Studentenprojekt!
In Vorlesung 4 (Refactoring) haben wir eine monolithische main.py in Module aufgeteilt:
road-profile-viewer/
├── src/
│ ├── geometry.py # Strahl-Schnitt-Berechnungen
│ ├── road.py # Profil-Generierung
│ ├── visualization.py # Chart-Erstellung
│ └── main.py # Anwendungs-Einstiegspunkt
└── tests/
Guter Start! Aber wir können mit einer echten Schichtenarchitektur weitergehen.
road-profile-viewer/
├── src/
│ ├── domain/ # Kern-Geschäftskonzepte
│ │ ├── models.py # RoadProfile, Measurement
│ │ └── services.py # GeometryCalculator
│ │
│ ├── infrastructure/ # Externe Systeme
│ │ └── repositories.py # ProfileRepository
│ │
│ ├── presentation/ # Benutzeroberfläche
│ │ ├── callbacks.py # Controller
│ │ └── charts.py # Views
│ │
│ └── api/ # Optional: REST API
│ └── routes.py # FastAPI-Endpunkte
Warum eine API, wenn Dash bereits funktioniert?
@app.get("/api/profiles/{profile_id}")
def get_profile(profile_id: str) -> ProfileResponse:
profile = repository.get(profile_id)
if not profile:
raise HTTPException(status_code=404)
return ProfileResponse(**profile.dict())
Falls ihr Road Profile Viewer jemals skalieren müsst:
| Änderung | Was zu ändern ist |
|---|---|
| Datenbank | SQLite durch PostgreSQL ersetzen — nur infrastructure/ ändern |
| Caching | Redis-Cache hinzufügen — neue Datei in infrastructure/ |
| Deployment | Mit Docker containerisieren — keine Code-Änderungen |
| Load Balancing | Mehrere Instanzen hinzufügen — nur Infrastruktur |
Die Schichtenarchitektur macht Änderungen lokal!
Szenario: Der Product Owner möchte "Profil als PDF exportieren" hinzufügen.
Beantwortet zu zweit:
5 Minuten, dann diskutieren wir!
Manche Entwickler glauben, Agile und Architektur stehen im Konflikt:
Extrem 1:
"Agile heißt kein Vorab-Design — einfach anfangen zu coden und refactoren!"
Extrem 2:
"Architektur erfordert monatelange Planung, bevor Code geschrieben wird!"
Beide Extreme sind falsch.
Der agile Ansatz: Emergentes Design mit absichtlicher Struktur.
Genug Entscheidungen treffen, um zu starten: Sprache, Framework, Grundstruktur wählen
Funktionierende Software implementieren: Echte Features bauen, keine hypothetische Infrastruktur
Refactorn, wenn Muster entstehen: Bei Duplikation umstrukturieren
Entscheidungen regelmäßig überprüfen: Sprint-Retros können technische Schulden adressieren
Ein technischer Spike ist eine zeitbegrenzte Untersuchung zur Reduktion von Unsicherheit.
Beispiel-Spike-Story:
Als Entwickler
möchte ich PostgreSQL vs SQLite für unsere Datenbank untersuchen
damit wir eine informierte Entscheidung treffen könnenZeitlimit: Maximal 2 Tage
Nach dem Spike: Eine informierte architektonische Entscheidung treffen, kein Raten.
| Konzept | Kernpunkt | Anwendung |
|---|---|---|
| Muster | Bewährte Lösungen für wiederkehrende Probleme | MVC für vorhersagbare Struktur |
| MVC | Model, View, Controller trennen | Pydantic-Models, Charts, Callbacks |
| Schichten | Präsentation → Geschäft → Daten | presentation/, domain/, infrastructure/ |
| Verteilt | Client-Server, Microservices | Vorerst monolithisch bleiben |
| Agile | Emergentes Design, technische Spikes | Entscheiden, implementieren, refactoren |
Muster sind bewährte Lösungen — MVC, Schichtenarchitektur und Client-Server lösen wiederkehrende Probleme
MVC trennt Belange — Model kennt Daten, View kennt Anzeige, Controller koordiniert
Schichten schaffen Grenzen — Abhängigkeiten fließen nur abwärts
Microservices sind nicht immer die Antwort — Kleine Teams profitieren von gut strukturierten Monolithen
Architektur entwickelt sich mit Agile — Einfach starten, refactoren wenn Muster entstehen
Muster-Identifikation: Welchem Muster folgt euer Road Profile Viewer derzeit?
Schicht-Zuordnung: Zu welcher Schicht gehört jede eurer Python-Dateien?
Abhängigkeitsrichtung: Zeigen alle eure Imports "abwärts"?
Skalierungs-Trigger: Welche Metrik würde euch sagen, dass es Zeit ist, von SQLite zu PostgreSQL zu wechseln?
Eure Aktionspunkte:
Aktuelles Muster identifizieren: Welchem Muster folgt euer Code (wenn überhaupt)?
Schichten zuordnen: Welcher Code ist Präsentation? Geschäft? Datenzugriff?
Eine Schicht refactoren: Den unordentlichsten Bereich wählen und Trennung anwenden
Architektur dokumentieren: Eine einfache ARCHITECTURE.md erstellen
API in Betracht ziehen: Würde eine REST-API eurem Projekt nützen?
Bücher:
Online:
Teil 1: Warum Architektur wichtig ist, 4+1 Sichtenmodell
Teil 2: Architekturmuster, Anwendung auf Road Profile Viewer
Das Ziel ist nicht perfekte Architektur — sondern absichtliche Architektur.