06 Übung: MVC-Refactoring
January 2026 (1217 Words, 7 Minutes)
Einführung
Das Model-View-Controller (MVC)-Muster trennt Code in drei Komponenten:
- Model: Daten und Geschäftslogik; weiß nichts über die Benutzeroberfläche
- View: Zeigt Daten dem Benutzer an; generiert HTML/visuelle Ausgabe
- Controller: Verarbeitet Benutzereingaben; koordiniert Model und View
Zusätzlich behandelt in Schichtenarchitekturen eine Repository/Datenzugriffsschicht Datenbankoperationen getrennt vom Model.
In dieser Übung analysieren Sie “Spaghetti-Code”, der alle Belange vermischt, und refaktorieren ihn in ordnungsgemäß getrennte Komponenten.
Lernziele:
- Vermischte Verantwortlichkeiten in monolithischem Code identifizieren
- Code in Model-, View-, Controller- und Repository-Komponenten aufteilen
- Die Verantwortlichkeiten jeder MVC-Komponente verstehen
- Saubere Komponenteninteraktionen entwerfen
Anweisungen:
- Studieren Sie die Referenz zur Separation of Concerns
- Analysieren Sie die bereitgestellte monolithische Funktion
- Bearbeiten Sie alle vier Aufgaben
- Überprüfen Sie Ihre Antworten anhand der Lösung
Gesamtpunkte: 20 Zeit: ~25 Minuten
Referenz: Separation of Concerns
| Komponente | Verantwortung | Sollte NICHT enthalten |
|---|---|---|
| Model | Geschäftsentitäten, Berechnungen, Regeln | SQL, HTML, HTTP-Verarbeitung |
| View | HTML-Generierung, Formatierung | Geschäftslogik, SQL |
| Controller | Anfrageverarbeitung, Koordination | Geschäftslogik, SQL, HTML |
| Repository | Datenbankabfragen, Persistenz | Geschäftslogik, HTML |
Zu analysierender Code
Die folgende Funktion vermischt Datenzugriff, Geschäftslogik und Präsentation in einer einzigen Funktion. Zeilennummern sind als Referenz angegeben.
1 def show_order_details(order_id):
2 # Connect to database and fetch order
3 conn = sqlite3.connect('shop.db')
4 cursor = conn.cursor()
5 order_row = cursor.execute(
6 "SELECT * FROM orders WHERE id = ?", (order_id,)
7 ).fetchone()
8
9 if not order_row:
10 return "<h1>Order Not Found</h1>"
11
12 # Fetch order items
13 items = cursor.execute(
14 "SELECT * FROM order_items WHERE order_id = ?", (order_id,)
15 ).fetchall()
16
17 # Calculate totals (business logic)
18 subtotal = sum(item['price'] * item['quantity'] for item in items)
19 tax = subtotal * 0.19 # 19% VAT
20 shipping = 0 if subtotal > 50 else 4.99
21 total = subtotal + tax + shipping
22
23 # Check if eligible for discount
24 if order_row['customer_type'] == 'premium':
25 total = total * 0.9 # 10% premium discount
26
27 # Generate HTML
28 html = f"""
29 <div class="order">
30 <h1>Order #{order_id}</h1>
31 <table>
32 <tr><th>Item</th><th>Qty</th><th>Price</th></tr>
33 """
34 for item in items:
35 html += f"<tr><td>{item['name']}</td><td>{item['quantity']}</td>"
36 html += f"<td>€{item['price']:.2f}</td></tr>"
37
38 html += f"""
39 </table>
40 <p>Subtotal: €{subtotal:.2f}</p>
41 <p>Tax (19%): €{tax:.2f}</p>
42 <p>Shipping: €{shipping:.2f}</p>
43 <p><strong>Total: €{total:.2f}</strong></p>
44 </div>
45 """
46 return html
Aufgabe A: Code-Abschnitte klassifizieren (6 Punkte)
Identifizieren Sie für jeden unten aufgeführten Code-Abschnitt, ob er zu M (Model), V (View), C (Controller) oder D (Datenzugriff/Repository) gehört.
| Zeilen | Klassifikation (M/V/C/D) | Kurze Begründung |
|---|---|---|
| 3-7 | ||
| 9-10 | ||
| 13-15 | ||
| 17-21 | ||
| 24-25 | ||
| 27-45 |
(1 Punkt pro korrekter Klassifikation)
Aufgabe B: Das Model entwerfen (5 Punkte)
Schreiben Sie eine Python-Klasse Order, die:
- Die Bestelldaten speichert (id, customer_type, items)
- Die Geschäftslogik für die Berechnung von Zwischensumme, Steuer, Versand und Gesamtsumme enthält
@property-Dekoratoren für berechnete Werte verwendet
Ihre Antwort:
# Schreiben Sie hier Ihre Order-Klasse
(Bewertung: 1 Pkt. Struktur, 2 Pkt. Properties, 2 Pkt. korrekte Logik)
Aufgabe C: Das Repository entwerfen (4 Punkte)
Schreiben Sie eine Python-Klasse OrderRepository mit einer Methode find_by_id(order_id), die:
- Die Datenbankabfragen ausführt
- Ein
Order-Objekt zurückgibt (oderNone, wenn nicht gefunden) - KEINE Geschäftslogik oder HTML enthält
Ihre Antwort:
# Schreiben Sie hier Ihre OrderRepository-Klasse
(Bewertung: 2 Pkt. korrekte Abfragen, 2 Pkt. korrekter Rückgabetyp)
Aufgabe D: Das Komponentendiagramm zeichnen (5 Punkte)
Zeichnen Sie ein Diagramm, das zeigt:
- Die vier Komponenten (Controller, Repository, Model, View)
- Pfeile, die zeigen, welche Komponente welche aufruft
- Beschriftungen an den Pfeilen, die beschreiben, was übergeben wird
Verwenden Sie ASCII-Art oder einfache Kästen und Pfeile.
Ihre Antwort:
(Zeichnen Sie hier Ihr Diagramm)
(Bewertung: 1 Pkt. pro korrekt dargestellter Komponente, 1 Pkt. für korrekte Pfeilrichtungen)
Lösung
Lösung anzeigen
Aufgabe A: Code-Klassifikation
| Zeilen | Klassifikation | Begründung |
|---|---|---|
| 3-7 | D (Datenzugriff) | Datenbankverbindung und SQL-Abfrage |
| 9-10 | C (Controller) | Ablaufsteuerung für Nicht-gefunden-Fall |
| 13-15 | D (Datenzugriff) | SQL-Abfrage für Items |
| 17-21 | M (Model/Geschäftslogik) | Berechnungslogik (Zwischensumme, Steuer, Versand) |
| 24-25 | M (Model/Geschäftslogik) | Geschäftsregel (Premium-Rabatt) |
| 27-45 | V (View) | HTML-Generierung |
Aufgabe B: Model-Klasse
@dataclass
class OrderItem:
name: str
quantity: int
price: float
@dataclass
class Order:
id: str
customer_type: str
items: list[OrderItem]
@property
def subtotal(self) -> float:
return sum(item.price * item.quantity for item in self.items)
@property
def tax(self) -> float:
return self.subtotal * 0.19
@property
def shipping(self) -> float:
return 0 if self.subtotal > 50 else 4.99
@property
def total(self) -> float:
base_total = self.subtotal + self.tax + self.shipping
if self.customer_type == 'premium':
return base_total * 0.9
return base_total
Bewertungshinweise:
- 1 Punkt: Hat id, customer_type, items Attribute
- 1 Punkt: Verwendet @property für subtotal
- 1 Punkt: Verwendet @property für tax und/oder shipping
- 1 Punkt: Korrekte subtotal-Berechnung
- 1 Punkt: Korrektes total mit Premium-Rabatt-Logik
Aufgabe C: Repository-Klasse
class OrderRepository:
def __init__(self, db_path: str):
self.db_path = db_path
def find_by_id(self, order_id: str) -> Order | None:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
order_row = cursor.execute(
"SELECT * FROM orders WHERE id = ?", (order_id,)
).fetchone()
if not order_row:
return None
item_rows = cursor.execute(
"SELECT * FROM order_items WHERE order_id = ?", (order_id,)
).fetchall()
items = [OrderItem(
name=row['name'],
quantity=row['quantity'],
price=row['price']
) for row in item_rows]
return Order(
id=order_id,
customer_type=order_row['customer_type'],
items=items
)
Bewertungshinweise:
- 1 Punkt: Korrekte Order-Abfrage
- 1 Punkt: Korrekte Items-Abfrage
- 1 Punkt: Gibt None zurück, wenn nicht gefunden
- 1 Punkt: Konstruiert und gibt Order-Objekt zurück
Aufgabe D: Komponentendiagramm
Benutzeranfrage
│
▼
┌─────────────────┐
│ Controller │ ← Koordiniert Ablauf
│ OrderController │
└────────┬────────┘
│ ruft find_by_id() auf
▼
┌─────────────────┐
│ Repository │ ← Datenzugriff
│ OrderRepository │
└────────┬────────┘
│ gibt Order zurück
▼
┌─────────────────┐
│ Model │ ← Geschäftslogik
│ Order │ (Berechnungen)
└────────┬────────┘
│ wird an render() übergeben
▼
┌─────────────────┐
│ View │ ← Präsentation
│ render_order() │
└────────┬────────┘
│
▼
HTML-Antwort
Bewertungshinweise:
- 1 Punkt: Controller oben dargestellt (empfängt Anfrage)
- 1 Punkt: Repository dargestellt (vom Controller aufgerufen)
- 1 Punkt: Model dargestellt (vom Repository zurückgegeben)
- 1 Punkt: View dargestellt (erhält Model)
- 1 Punkt: Korrekte Pfeilrichtungen (Abwärtsfluss)
Alternative akzeptable Diagrammstile:
Controller → Repository → (gibt zurück) Model
↓ ↓
View ←──────────────────────┘
Oder horizontal:
Anfrage → Controller → Repository → DB
↓ ↓
View ← ← ← ← Order