flowchart LR
C["Checkout"] --> S["Zahlart: name und gebuehr"]
S --> K["Karte"]
S --> P["PayPal"]
S --> R["Rechnung"]
S --> X["neue Zahlart, ohne Checkout zu ändern"]
Design-Prinzipien und Patterns: Coupling, Cohesion, SOLID, Komposition, Strategy, Observer, Adapter, Repository
Track Konzepte · Design und Architektur · ca. 65 Min.
Worum es geht
Du bekommst eine kleine Änderung: “Wir brauchen eine neue Zahlart.” Und dann fasst du fünf Dateien an, vergisst die sechste, und in Produktion kommt auf dem Beleg ein Fragezeichen heraus. Das ist kein Pech, sondern ein Designproblem: Die Teile des Systems sind eng gekoppelt (tight coupling), und eine Änderung zieht mehrere Module nach sich.
Am Ende der Lektion kannst du Coupling (Kopplung) und Cohesion (Zusammenhalt) erklären und Kopplungssymptome erkennen. Du hast vier Patterns selbst gebaut oder repariert: Dependency Injection mit Adapter, Strategy, Observer und Repository. Und du weißt, wann du sie nicht einsetzt. Patterns sind eine gemeinsame Sprache im Team, keine Pflicht. Wer sie vorsorglich überall einbaut, erzeugt Overengineering (zu viel Bauwerk für zu wenig Problem).
Diese Lektion deckt die zweite Hälfte des Prüfsteins der Schicht “Design und Architektur” ab: Woran erkennst du, dass eine Grenze zwischen Modulen falsch gezogen ist? (Die erste Hälfte, Monolith gegen Microservices, kommt in einer eigenen Lektion.)
Plane ehrlich 65 Minuten ein: etwa 25 Minuten Lesen, 40 Minuten Übungen (fünf Stück). Eine gute Pause ist nach Übung 2.
Von JS/TS her gedacht
Du kennst diese Ideen aus React, ohne sie so zu nennen:
| Konzept | JS/TS | Python (hier) |
|---|---|---|
| Komposition statt Vererbung | children und Props, Komponenten ineinander stecken |
Objekte aus Teilen zusammensetzen, Funktionen als Argumente |
| Strategy | Comparator bei sort, ein Custom Hook, der ein Verhalten kapselt |
sorted(key=...), Objekt oder Funktion als Argument |
| Observer | addEventListener, Store-Subscriptions, useEffect-Cleanup |
Liste von Callbacks mit Abmelde-Funktion |
| Dependency Injection | Props, Context, fetch als Parameter für Tests |
Konstruktor-Argument |
| Adapter | Wrapper um ein SDK mit anderem Datenformat | Klasse, die eine fremde Schnittstelle auf deine abbildet |
| Repository | Datenzugriffs-Modul statt fetch in der Komponente |
Klasse mit finde() und speichern() |
Strategy und Komposition in React, als Skizze (nicht ausgeführt, nur zur Orientierung):
// Die Dialog-Komponente kennt keinen Inhalt. Der Inhalt wird "hineingesteckt".
function Dialog({ title, children, footer }: Props) {
return <section><h2>{title}</h2>{children}<footer>{footer}</footer></section>;
}
// Ein Hook als Strategy: die Komponente weiß nicht, WIE sortiert wird.
const sorted = useSortedList(items, byPrice); // byPrice ist die Strategie
Und die Strategy in reinem JavaScript, mit Node ausgeführt. Dasselbe Konzept hast du schon tausendmal benutzt:
const nachLaenge = (a, b) => a.length - b.length;
const nachAlphabet = (a, b) => a.localeCompare(b);
const wörter = ["bb", "a", "ccc"];
console.log([...wörter].sort(nachLaenge), [...wörter].sort(nachAlphabet).reverse());Ausgabe:
[ 'a', 'bb', 'ccc' ] [ 'ccc', 'bb', 'a' ]
sort kennt das Vergleichskriterium nicht, du reichst es hinein. Das ist Strategy in ihrer kleinsten Form.
Konzept
Schritt 1: Komplexität, Coupling, Cohesion
Der Hauptfeind beim Entwickeln ist Komplexität (complexity): Du kannst nicht mehr im Kopf behalten, was eine Änderung auslöst. Design-Prinzipien sind Werkzeuge, um Komplexität klein zu halten. Vier Begriffe musst du kennen:
- Coupling (Kopplung): Wie stark hängt ein Modul von den Details eines anderen ab? Lose gekoppelt heißt: Ich kann das eine ändern, ohne das andere anzufassen.
- Cohesion (Zusammenhalt): Gehört das, was in einem Modul steht, fachlich zusammen? Hohe Cohesion ist gut: Eine Klasse hat einen Grund, sich zu ändern.
- Abstraktion (abstraction): Du benutzt etwas über das, was es tut, nicht wie.
sorted(...)benutzt du, ohne den Algorithmus zu kennen. - Information Hiding (Verbergen von Details): Ein Modul zeigt nach außen nur eine kleine Schnittstelle. Alles andere darf sich ändern, ohne dass jemand es merkt.
Ziel ist lose Kopplung und hohe Cohesion. Wie sieht das Gegenteil aus? Hier ein Checkout, der bewusst schlecht gebaut ist. Lies ihn und suche die Probleme:
Ausgabe:
SMTP an ayse@example.com : Kartenzahlung: 100.00 EUR + 2.00 EUR Gebühr
Zwei Kopplungssymptome, die du in jedem Code Review suchen kannst:
- Änderungen ziehen mehrere Stellen nach sich (Shotgun Surgery, Änderungsverstärkung): Jede neue Zahlart steckt in zwei If-Ketten. Wer nur eine ändert, baut einen stillen Fehler ein. Das passiert hier:
Ausgabe: '?: 100.00 EUR + 0.35 EUR Gebühr'. Das Fragezeichen auf dem Beleg ist genau der Fehler aus der Einleitung.
- Direkte Abhängigkeit auf Konkretes, schwer testbar:
CheckouterzeugtSmtpMailselbst. Jeder Test vonbezahlenwürde eine echte Mail verschicken. Du kannst die Mail im Test weder abfangen noch ersetzen.
Das sind die Antworten auf die Frage “woran erkenne ich, dass eine Grenze falsch gezogen ist?”: Du änderst eine fachliche Sache und musst mehrere Module anfassen. Oder du kannst ein Modul nur testen, wenn du das halbe System mitstartest.
Schritt 2: Dependency Injection und Adapter
Dependency Injection (DI, Abhängigkeiten von außen übergeben) löst Problem 1: Die Klasse bekommt ihre Abhängigkeit im Konstruktor, statt sie selbst zu erzeugen. Das ist die Technik hinter dem D aus SOLID, Dependency Inversion: Hochwertige Fachlogik hängt von einer kleinen Schnittstelle ab, nicht von einer konkreten Technik.
In Python brauchst du dafür keine Interface-Deklaration. Jedes Objekt mit einer Methode send(an, text) passt (Duck Typing). Für den Test bauen wir ein Fake (Testdouble, das Aufrufe nur aufzeichnet):
Ausgabe: [('ayse@example.com', 'PayPal: 100.00 EUR + 3.35 EUR Gebühr')]. Kein Netzwerk, der Test prüft den Inhalt der Mail.
Was, wenn die Schnittstelle der fremden Bibliothek anders aussieht als die, die du dir wünschst? Dann schreibst du einen Adapter (adapter): eine kleine Klasse, die deine Schnittstelle anbietet und die fremde intern aufruft. Beispiel: Ein Wetterdienst liefert Fahrenheit in einem Dict. Deine Heizungslogik will celsius(ort):
Ausgabe: 20.0 False. 68 Grad Fahrenheit sind 20 Grad Celsius. Der Gewinn: Fahrenheit und die Dict-Schlüssel der Fremdfirma stehen an einer Stelle. Wechselst du den Anbieter, schreibst du einen neuen Adapter, Heizung bleibt unberührt. Das ist Information Hiding.
Schritt 3: Strategy statt If-Kette
Problem 2 aus Schritt 1 (zwei If-Ketten) löst Strategy (Strategie): Jede Variante eines Verhaltens wird ein eigenes kleines Objekt mit derselben Schnittstelle. Die Fachlogik ruft nur die Schnittstelle auf und wählt nicht selbst aus.
Ausgabe:
Kartenzahlung: 100.00 EUR + 2.00 EUR Gebühr
SEPA-Lastschrift: 100.00 EUR + 0.35 EUR Gebühr
Das ist das O aus SOLID, das Open/Closed-Prinzip: offen für Erweiterung (neue Klasse Sepa), geschlossen für Änderung (Checkout2 bleibt unangetastet). Das Fragezeichen kann gar nicht mehr entstehen, weil Name und Gebühr in einer Klasse liegen (hohe Cohesion).
In Python ist eine Strategy oft einfach eine Funktion (sorted(key=...)), du brauchst keine Klasse, wenn kein Zustand dabei ist. Wo kommt die Strategy her? Meist entscheidet eine Stelle am Rand des Systems, welche Variante erzeugt wird. Das nennt man Factory (Fabrik): ein kleines Register vom Namen zur Klasse.
Ausgabe: 3.35. Der Name "paypal" kommt aus dem Formular oder der Konfiguration, danach arbeitet alles mit Objekten. Genau dieses Register baust du in Übung 2 selbst.
Schritt 4: Observer und die Abmelde-Falle
Observer (Beobachter) trennt den, der etwas meldet, von denen, die darauf reagieren. Eine Quelle (Subject) führt eine Liste von Beobachtern. Ändert sich etwas, ruft sie jeden Beobachter auf. Die Quelle kennt die Beobachter nicht, sie weiß nur: “Das sind Funktionen, die ich aufrufe.” Neue Reaktionen (Mail, Statistik, Log) kommen dazu, ohne die Quelle zu ändern. In JS ist das addEventListener.
Die einfachste Version:
Ausgabe:
A 1
einmalig 1
A 2
C 2
Schau genau hin: Bei Ereignis 1 fehlt C 1. Der Beobachter C hat die Nachricht nie bekommen, ohne dass ein Fehler auftrat. Die Ursache: for fn in self.beobachter läuft über die Liste, während einmalig ein Element aus derselben Liste entfernt. Die Schleife springt dadurch ein Element weiter. Dasselbe passiert in JS, wenn du ein Array beim forEach veränderst.
Die Lösung ist, über eine Kopie (Schnappschuss) zu laufen: for fn in list(self.beobachter). Damit gilt eine klare Regel: Wer zu Beginn der Meldung angemeldet war, bekommt sie. Wer währenddessen hinzukommt, bekommt erst die nächste.
Noch eine Designentscheidung: ab(fn) braucht die Funktion, die du anmelden wolltest. Bequemer ist es, wenn an(fn) eine Abmelde-Funktion zurückgibt, genau wie useEffect eine Cleanup-Funktion. Das baust du in Übung 3.
Schritt 5: Repository
Ein Repository versteckt, wie Daten gespeichert werden, hinter einer kleinen Schnittstelle in der Sprache der Fachlogik (finde, speichern). Die Fachlogik weiß nichts von SQL. Das bringt dir zwei Dinge: Du kannst die Fachlogik gegen ein Fake-Repository (nur ein Dict im Speicher) testen, ohne Datenbank. Und du kannst die Datenbank später wechseln, ohne die Fachlogik anzufassen.
Ausgabe:
FakeKundenRepo Ayse ('Ayse', False)
SqliteKundenRepo Ayse ('Ayse', False)
Beide Repositories verhalten sich gleich, Kundenservice hat den Unterschied nie bemerkt. Achte auf zwei Details der SQL-Version: fetchone() liefert None, wenn nichts gefunden wurde (das musst du abfangen), und SQLite kennt keinen Bool-Typ, du speicherst 0 und 1 und wandelst zurück.
Schritt 6: Komposition statt Vererbung, SOLID, YAGNI
Komposition vor Vererbung (composition over inheritance): Verhalten setzt du aus Teilen zusammen, die ein Objekt hat, statt es von einer Elternklasse zu erben. Vererbung koppelt eng (die Unterklasse hängt an den Interna der Elternklasse) und explodiert bei Kombinationen: Zwei unabhängige Zusatzfunktionen ergeben vier Klassen, drei ergeben acht. Mit Komposition steckst du die Teile zur Laufzeit zusammen:
Ausgabe:
[FERTIG]
[fertig]
fertig
Eine Klasse, jede Kombination. Das ist dein React-Alltag: Du baust Komponenten aus Komponenten und erbst fast nie.
Die SOLID-Prinzipien sind fünf Faustregeln, die das Obige zusammenfassen. Du musst sie als Begriffe kennen (Prüfungen und Interviews fragen danach):
| Buchstabe | Prinzip | In einem Satz | Wo du es gesehen hast |
|---|---|---|---|
| S | Single Responsibility | Eine Klasse hat einen Grund, sich zu ändern | Zahlart-Klassen, Repository |
| O | Open/Closed | Erweitern ohne Ändern | Sepa ohne Änderung von Checkout2 |
| L | Liskov Substitution | Eine Unterklasse muss überall passen, wo die Oberklasse erwartet wird | Jede Zahlart ist austauschbar |
| I | Interface Segregation | Lieber kleine, passende Schnittstellen als eine große | Repository mit 2 bis 3 Methoden |
| D | Dependency Inversion | Fachlogik hängt von Schnittstellen ab, nicht von Technik | Checkout(mail), Kundenservice(repo) |
Dazu zwei Faustregeln: DRY (Don’t Repeat Yourself, jede Information nur an einer Stelle) und YAGNI (You Aren’t Gonna Need It, baue nichts für Anforderungen, die es noch nicht gibt).
Und jetzt der wichtigste Teil: Alles Obige ist ein Werkzeug, kein Ziel. Hier das Gegenbeispiel. Jemand will “Hallo Name” ausgeben, “falls später andere Sprachen kommen”:
Beide geben Hallo Gökay aus. Die erste Version hat 3 Klassen und 10 Zeilen, die zweite 1 Funktion und 2 Zeilen. Es gibt nur eine Sprache, also ist die ganze Struktur Ballast. Merke dir die Rule of Three (allgemeines Fachwissen): Beim ersten Mal schreibst du es direkt. Beim zweiten Mal duplizierst du notfalls. Erst beim dritten Mal, wenn du sicher siehst, was variiert, baust du die Abstraktion. Denn eine falsche Abstraktion ist teurer als etwas Duplikat: Sie zieht jede spätere Änderung in die falsche Form.
Der Test für ein Pattern ist darum nie “Habe ich eines benutzt?”, sondern “Welchen Schmerz löst es hier?” Die Schmerzen aus Schritt 1 (Änderung an vielen Stellen, kein Test ohne Netzwerk) waren real. Der Begrüßer hat keinen.
Falle
- Pattern-Sammlung statt Problemlösung. Factory, Strategy und Repository für ein Skript mit einer Datenquelle. Erst der Schmerz, dann das Pattern.
- Observer: Abmelden während der Benachrichtigung. Die Liste wird verändert, während du sie durchläufst, und ein Beobachter bekommt die Nachricht still nicht (Schritt 4).
- Vererbung für Wiederverwendung. Eine Unterklasse nur, um Code zu übernehmen, koppelt eng und explodiert bei Kombinationen. Frage: “Ist das ein Sonderfall von X (is-a) oder hat es einen X (has-a)?”
- Fachlogik, die SQL oder Netzwerk kennt. Dann ist sie nur mit Datenbank testbar. Ein Test, der ohne Fake nicht läuft, ist ein Kopplungssymptom.
- Falsch verstandenes DRY. Zwei Codestücke, die nur zufällig gleich aussehen, zusammenzulegen, koppelt Dinge, die sich unabhängig ändern sollten.
Übungen
Übung 1: Kaputten Code reparieren, Adapter und Injection (ca. 8 Min.)
Ein Mahnwesen verschickt Zahlungserinnerungen. Es erzeugt die fremde Mail-Bibliothek FremdMailer selbst und kennt deren HTML-Schnittstelle. Ein Test würde echte Mails verschicken. FremdMailer gehört nicht dir und darf nicht geändert werden. Sie hat die Methode send_raw(to_addr, subject, body_html).
Reparatur in zwei Teilen:
- Deine eigene Schnittstelle heißt
senden(an, betreff, text)mit reinem Text (kein HTML).Mahnwesenbekommt einenversendermit dieser Schnittstelle im Konstruktor und benutzt nurversender.senden(...).Mahnwesenweiß nichts vonFremdMailerund nichts von HTML. MailAdapter(fremd)bietetsenden(an, betreff, text)an und ruft internfremd.send_raw(an, betreff, "<p>" + text + "</p>")auf.
Die letzte Zeile gibt beide Klassen zurück, so ruft der Check sie ab.
Wer erzeugt in der reparierten Version den Versender, und wer kennt als einziges das HTML? Das Format der Nachricht gehört in die Klasse, die die fremde Schnittstelle kennt.
class Mahnwesen:
def __init__(self, versender):
self.versender = versender
def mahnen(self, kunde, betrag):
text = f"Hallo {kunde['name']}, offen: {betrag:.2f} EUR"
self.versender.senden(kunde["email"], "Zahlungserinnerung", text)
return True
class MailAdapter:
def __init__(self, fremd):
self.fremd = fremd
def senden(self, an, betreff, text):
self.fremd.send_raw(an, betreff, "<p>" + text + "</p>")
Mahnwesen, MailAdapterMahnwesen hängt jetzt nur noch an der eigenen, kleinen Schnittstelle senden. Alles Fremde (Methodenname, HTML) steckt im Adapter. Im Test genügt ein Fake mit senden.
Übung 2: Strategy statt If-Kette, offen für Neues (ca. 8 Min.)
Ein Ticketshop berechnet Fahrpreise mit dieser If-Kette:
def fahrpreis(kategorie, basis):
if kategorie == "erwachsene":
return basis
elif kategorie == "kind":
return round(basis * 0.5, 2)
elif kategorie == "senior":
return round(basis * 0.8, 2)
raise ValueError(kategorie)Jede neue Kategorie bedeutet: diese Funktion ändern. Baue einen Preisrechner, der nach dem Open/Closed-Prinzip arbeitet:
registriere(name, regel)meldet eine Preisregel an.regelist ein Aufruf (Funktion oder Objekt mit__call__), derbasisbekommt und den Preis zurückgibt. Meldest du denselben Namen nochmal an, ersetzt die neue Regel die alte.preis(name, basis)ruft die Regel auf und gibt ihr Ergebnis unverändert zurück. Ein unbekannter Name löstValueErroraus (keinKeyError).- Jeder
Preisrechnerhat seine eigenen Regeln.
Der Check meldet danach Regeln an, die du nicht kennst (neue Kategorien), und ruft sie auf. Die letzte Zeile gibt die Klasse zurück.
Wo sollen die Regeln leben, damit zwei Rechner sich nicht gegenseitig beeinflussen? Und welcher Fehler entsteht von selbst, wenn du einen unbekannten Namen in einem Dict nachschlägst, und ist es genau der, den die Aufgabe verlangt?
class Preisrechner:
def __init__(self):
self.regeln = {}
def registriere(self, name, regel):
self.regeln[name] = regel
def preis(self, name, basis):
try:
regel = self.regeln[name]
except KeyError:
raise ValueError(f"unbekannte Kategorie: {name}")
return regel(basis)
PreisrechnerDas Dict gehört in __init__ (pro Objekt), nicht in den Klassenrumpf (das wäre geteilt zwischen allen Rechnern). Der Rechner kennt keine einzige Kategorie, neue Preisregeln brauchen keine Änderung an der Klasse.
Übung 3: Observer mit Abmelde-Funktion (ca. 8 Min.)
Ein Lager meldet Bestandsänderungen. Schreibe die Klasse:
Lager(menge)startet mit einer Menge.beobachte(fn)meldetfnan und gibt eine Abmelde-Funktion zurück. Ruft man sie ein zweites Mal auf, passiert nichts (kein Fehler).setze(neu)ändert die Menge. Istneugleich der alten Menge, passiert nichts. Sonst ruft es jeden Beobachter mitfn(alt, neu)auf, in der Reihenfolge der Anmeldung.- Wer zu Beginn von
setzeangemeldet ist, bekommt die Meldung, auch wenn sich ein Beobachter währenddessen abmeldet. Wer währenddessen neu angemeldet wird, bekommt sie erst beim nächstensetze.
Die letzte Zeile gibt die Klasse zurück.
Was passiert mit einer Schleife, wenn ein Beobachter die Liste verändert, über die sie gerade läuft? Und was sollte abmelden tun, wenn der Beobachter schon weg ist?
class Lager:
def __init__(self, menge):
self.menge = menge
self.beobachter = []
def beobachte(self, fn):
self.beobachter.append(fn)
def abmelden():
if fn in self.beobachter:
self.beobachter.remove(fn)
return abmelden
def setze(self, neu):
alt = self.menge
if neu == alt:
return
self.menge = neu
for fn in list(self.beobachter):
fn(alt, neu)
Lagerlist(self.beobachter) ist der Schnappschuss: An- und Abmelden verändert die echte Liste, aber nicht die, über die gerade iteriert wird.
Übung 4: Repository mit sqlite, Fachlogik gegen Fake testbar (ca. 10 Min.)
Eine Bibliothek verleiht Bücher. Du schreibst zwei Dinge:
Bibliothek(repo) ist die Fachlogik und kennt nur diese drei Repository-Methoden:
repo.ausgeliehen_von(buch_id)gibt den Namen der Person zurück oderNone.repo.speichern(buch_id, person)undrepo.loeschen(buch_id).
Regeln: verleihen(buch_id, person) löst ValueError aus, wenn das Buch schon verliehen ist, sonst speichert es. zurueckgeben(buch_id) löst ValueError aus, wenn das Buch gar nicht verliehen ist, sonst löscht es den Eintrag und gibt den Namen der Person zurück.
SqliteLeihRepo(con) setzt diese drei Methoden mit sqlite um. Die Tabelle wird im Konstruktor schon angelegt.
Der Check lässt Bibliothek zuerst gegen ein Fake-Repository (nur ein Dict, kein sqlite) laufen und danach gegen dein SqliteLeihRepo. Beides muss dasselbe Verhalten zeigen. Die letzte Zeile gibt beide Klassen zurück.
Die Fachlogik darf execute und con nicht einmal erwähnen. Und was liefert fetchone() zurück, wenn die Abfrage keine Zeile findet, und was, wenn doch?
class Bibliothek:
def __init__(self, repo):
self.repo = repo
def verleihen(self, buch_id, person):
if self.repo.ausgeliehen_von(buch_id) is not None:
raise ValueError(f"Buch {buch_id} ist schon verliehen")
self.repo.speichern(buch_id, person)
def zurueckgeben(self, buch_id):
person = self.repo.ausgeliehen_von(buch_id)
if person is None:
raise ValueError(f"Buch {buch_id} ist nicht verliehen")
self.repo.loeschen(buch_id)
return person
class SqliteLeihRepo:
def __init__(self, con):
self.con = con
con.execute("CREATE TABLE leihe (buch_id INTEGER PRIMARY KEY, person TEXT)")
def ausgeliehen_von(self, buch_id):
zeile = self.con.execute("SELECT person FROM leihe WHERE buch_id = ?", (buch_id,)).fetchone()
return None if zeile is None else zeile[0]
def speichern(self, buch_id, person):
self.con.execute("INSERT OR REPLACE INTO leihe VALUES (?, ?)", (buch_id, person))
def loeschen(self, buch_id):
self.con.execute("DELETE FROM leihe WHERE buch_id = ?", (buch_id,))
Bibliothek, SqliteLeihRepoDie Regeln (schon verliehen, nicht verliehen) stehen in der Fachlogik und laufen mit jedem Repository. Das Repository enthält nur Speichern und Lesen.
Übung 5: Komposition und Overengineering (ca. 6 Min.)
Zwei Fälle mit Randbedingungen. Gib ein Tupel mit zwei Buchstaben zurück, zum Beispiel ("A", "B").
Fall 1. Ein Exportmodul hat die Basisklasse Exporter mit den Unterklassen CsvExporter und JsonExporter. Neu: Jede Ausgabe soll wahlweise verschlüsselt und/oder komprimiert werden. Welche Kombination gilt, entscheidet sich pro Kunde zur Laufzeit. Die Schritte sind voneinander unabhängig, und es werden voraussichtlich weitere Schritte folgen. Welcher Entwurf passt?
- A. Pro Kombination eine Unterklasse anlegen, etwa
CsvVerschluesseltoderJsonKomprimiert, und neue Kombinationen jeweils ergänzen. - B. Eine Klasse von beiden Formaten erben lassen (Mehrfachvererbung) und die Schritte über eine Konfigurationsdatei einschalten.
- C. In die Basisklasse zwei Flags
verschluesselnundkomprimiereneinbauen, die jede Unterklasse selbst abfragt und umsetzt. - D. Den Exporter aus einem Format-Objekt und einer Liste von Nachbearbeitungsschritten zusammensetzen, die je Kunde gewählt wird.
Fall 2. Du schreibst ein Skript für einen einmaligen Jahresbericht: Es liest eine CSV-Datei, rechnet Summen und schreibt eine Textdatei. Es läuft zweimal, und du arbeitest allein daran. Ein Kollege schlägt “falls später andere Quellen kommen” ein Repository-Interface, eine Strategy für die Berechnung und eine Factory für die Ausgabe vor. Keine zweite Quelle ist geplant. Die Summen müssen aber einzeln testbar sein, weil falsche Zahlen im Bericht Folgen hätten. Was machst du?
- A. Drei kleine Funktionen (lesen, rechnen, schreiben) ohne Interfaces bauen und erst umbauen, wenn eine zweite Quelle kommt.
- B. Die Vorschläge komplett umsetzen, damit der Code später ohne Umbau um neue Quellen und Ausgaben erweitert werden kann.
- C. Alles in eine einzige lange Funktion schreiben, weil das Skript nur zweimal läuft und sich Struktur dafür nicht lohnt.
- D. Nur das Repository-Interface bauen, weil Datenzugriff immer abstrahiert gehört, Strategy und Factory dagegen weglassen.
Fall 1: Wie viele Klassen brauchst du bei jedem weiteren Schritt, und wann steht die Kombination fest? Fall 2: Welchen Schmerz würde jedes vorgeschlagene Pattern hier heute lösen?
antwort = ("D", "A")
antwortFall 1, D: Format und Schritte sind unabhängige Teile, die zur Laufzeit zusammengesteckt werden. Ein neuer Schritt ist eine neue Funktion oder Klasse, keine Klassenexplosion. Fall 2, A: Kleine Funktionen mit hoher Cohesion lösen das Testproblem, Interfaces und Factories lösen heute keinen Schmerz (YAGNI). Kommt die zweite Quelle, baust du dann um und weißt, was variiert.
Merksatz
Koppele lose und halte zusammen, was zusammengehört: Gib Abhängigkeiten von außen hinein, setze Verhalten aus Teilen zusammen (Strategy, Observer, Adapter, Repository) und nimm ein Pattern nur dann, wenn es einen echten Schmerz löst.
Prüfstein
- Woran erkennst du, dass eine Grenze zwischen Modulen falsch gezogen ist? Nenne zwei Symptome und beschreibe, wie du eines davon behebst.
- Ein Kollege will für ein kleines Skript Repository, Strategy und Factory einbauen “für später”. Was fragst du ihn, und wann ist deine Antwort “ja”?
Quelle: quellen/konzeptuebersicht-software-grundlagen.md, Abschnitt “6. Design und Architektur” (Grundprinzipien: Coupling und Cohesion, Abstraktion, Information Hiding, Komplexität als Hauptfeind; Faustregeln SOLID, DRY, YAGNI, Komposition vor Vererbung; Design Patterns Strategy, Observer, Factory, Adapter, Repository “als gemeinsame Sprache, nicht als Pflicht”; Prüfstein zur falsch gezogenen Modulgrenze, hier nur der Teil zu den Kopplungssymptomen).
Über die Quelle hinaus (allgemeines Fachwissen): die Definitionen von Coupling und Cohesion, die Kopplungssymptome (Shotgun Surgery, schwer testbar), die Erklärung der fünf SOLID-Buchstaben, Dependency Injection und Duck Typing, Fakes als Testdoubles, die Rule of Three, die Aussage “falsche Abstraktion ist teurer als Duplikat”, die React-Skizze (nicht ausgeführt), die Gefahr beim Verändern einer Liste während der Iteration (hier mit Python ausgeführt), die Klassenexplosion bei Vererbung. Alle Code-Ausgaben in dieser Lektion stammen aus dem Ausführen des Codes mit Python 3 beziehungsweise Node.