Adapter & Decorator
Zwei Structural Patterns im Detail: Adapter fuer Interface-Anpassungen, Decorator fuer das dynamische Erweitern von Verhalten.
Inhaltsverzeichnis
Adapter & Decorator
Structural Patterns kuemmern sich darum, wie Objekte zusammengesetzt werden. Adapter und Decorator sind die beiden wichtigsten.
Adapter Pattern
Problem: Du hast zwei Komponenten, die nicht zusammenpassen - verschiedene Interfaces. Du willst nicht beide aendern.
Loesung: Ein Adapter (wie ein Reise-Stromadapter) uebersetzt zwischen beiden.
Konkretes Beispiel
Angenommen, deine App erwartet einen Logger mit dieser Signatur:
class Logger:
def log(self, level: str, message: str): ...
Aber die Library, die du nutzen willst, hat:
class ThirdPartyLogger:
def write_info(self, msg): ...
def write_error(self, msg): ...
def write_debug(self, msg): ...
Die Interfaces passen nicht zusammen. Der Adapter:
class ThirdPartyLoggerAdapter(Logger):
def __init__(self, third_party: ThirdPartyLogger):
self.third_party = third_party
def log(self, level: str, message: str):
if level == "info":
self.third_party.write_info(message)
elif level == "error":
self.third_party.write_error(message)
elif level == "debug":
self.third_party.write_debug(message)
else:
self.third_party.write_info(f"[{level}] {message}")
Jetzt kann deine App den Third-Party-Logger nutzen, ohne dass sie ihn kennt.
Wann nutzen?
- Du integrierst externe Libraries mit unpassenden APIs
- Du migrierst langsam von alt zu neu und willst beides parallel nutzen
- Du moechtest ein Test-Double haben, das eine fremde API simuliert
Ein zweites Beispiel
Legacy-Code hat Kilometer-basierte API, aber die neue Library spricht Meilen:
class MeilenService:
def laufen(self, meilen: float) -> str:
return f"Du bist {meilen} Meilen gelaufen."
class KilometerAdapter:
def __init__(self, meilen_service: MeilenService):
self.meilen_service = meilen_service
def laufen(self, km: float) -> str:
meilen = km * 0.621371
return self.meilen_service.laufen(meilen)
service = KilometerAdapter(MeilenService())
print(service.laufen(5.0))
In der Praxis
- Database Drivers: SQLAlchemy nutzt Adapter fuer verschiedene DBs (Postgres, MySQL, SQLite)
- Logging-Libraries: SLF4J (Java),
log4jsbridgen zwischen Frameworks - Testing: Mocks sind Adapter, die APIs simulieren
Decorator Pattern
Problem: Du willst ein Objekt dynamisch erweitern, ohne es zu veraendern oder viele Subklassen anzulegen.
Loesung: Ein Decorator wraps das Objekt und fuegt Verhalten hinzu.
Klassisches Beispiel: Kaffee mit Extras
from abc import ABC, abstractmethod
class Kaffee(ABC):
@abstractmethod
def kosten(self) -> float: ...
@abstractmethod
def beschreibung(self) -> str: ...
class EinfacherKaffee(Kaffee):
def kosten(self): return 2.50
def beschreibung(self): return "Einfacher Kaffee"
class KaffeeDecorator(Kaffee):
def __init__(self, kaffee: Kaffee):
self.kaffee = kaffee
class MitMilch(KaffeeDecorator):
def kosten(self): return self.kaffee.kosten() + 0.50
def beschreibung(self): return self.kaffee.beschreibung() + " + Milch"
class MitZucker(KaffeeDecorator):
def kosten(self): return self.kaffee.kosten() + 0.30
def beschreibung(self): return self.kaffee.beschreibung() + " + Zucker"
class MitSchokolade(KaffeeDecorator):
def kosten(self): return self.kaffee.kosten() + 1.00
def beschreibung(self): return self.kaffee.beschreibung() + " + Schoko"
# Zusammensetzen
kaffee = EinfacherKaffee()
kaffee = MitMilch(kaffee)
kaffee = MitZucker(kaffee)
kaffee = MitSchokolade(kaffee)
print(kaffee.beschreibung()) # "Einfacher Kaffee + Milch + Zucker + Schoko"
print(kaffee.kosten()) # 4.30
Jede Kombination moeglich - ohne Kombinatorik-Explosion mit Unterklassen.
Decorator als Funktions-Erweiterung
In Python / JS sind Funktionen First-Class - Decorator-Pattern geht auch fuer Funktionen:
def log_aufruf(fn):
def wrapper(*args, **kwargs):
print(f"Aufruf {fn.__name__} mit {args}")
result = fn(*args, **kwargs)
print(f"Ergebnis: {result}")
return result
return wrapper
@log_aufruf
def addiere(a, b):
return a + b
addiere(3, 4)
# Aufruf addiere mit (3, 4)
# Ergebnis: 7
Python-Decorators sind Sprach-gestuetzte Instanz des Patterns. Das @log_aufruf ist syntaktischer Zucker fuer addiere = log_aufruf(addiere).
Decorator-Beispiele aus dem Alltag
from functools import wraps, lru_cache
import time
@lru_cache(maxsize=None)
def fibonacci(n):
if n < 2: return n
return fibonacci(n-1) + fibonacci(n-2)
def timing(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
t0 = time.time()
result = fn(*args, **kwargs)
print(f"{fn.__name__}: {time.time() - t0:.3f}s")
return result
return wrapper
@timing
def langsam():
time.sleep(1)
langsam() # "langsam: 1.001s"
@lru_cache ist Memoisierung via Decorator. @timing misst Laufzeit. Beides: Pattern in Action.
In JavaScript / TypeScript
TypeScript hat eingebaute Decorator-Syntax (@decorator):
function log<T extends { new (...args: any[]): {} }>(constructor: T) {
console.log(`Klasse ${constructor.name} erstellt`);
return constructor;
}
@log
class Service {}
In React werden Higher-Order Components (HOCs) als Decorator genutzt:
function withAuth(Component) {
return function WithAuth(props) {
const user = useAuth();
if (!user) return <LoginPrompt />;
return <Component {...props} user={user} />;
};
}
const ProtectedDashboard = withAuth(Dashboard);
Wann ist Decorator sinnvoll?
- Cross-Cutting Concerns: Logging, Caching, Auth, Rate-Limiting
- Aenderung ohne Vererbung: vermeidet tiefe Klassen-Hierarchien
- Kombinierbar: mehrere Decorators gestapelt
Decorator vs. Inheritance
| Situation | Inheritance | Decorator |
|---|---|---|
| Zur Compile-Zeit fixes Verhalten | โ | |
| Dynamische Kombination | โ | |
| Eine โist-einโ-Beziehung | โ | |
| Features an- und ausschalten | โ | |
| Mehrere Aspekte kombinieren | โ |
Facade Pattern
Verwandt mit Adapter: Facade verbirgt die Komplexitaet eines Subsystems hinter einer einfachen Schnittstelle.
# Subsystem mit mehreren Komponenten
class Cpu:
def start(self): ...
def execute(self): ...
def stop(self): ...
class Memory:
def load(self, address, data): ...
class HardDrive:
def read(self, block): ...
# Facade
class Computer:
def __init__(self):
self.cpu = Cpu()
self.memory = Memory()
self.hd = HardDrive()
def start(self):
self.cpu.start()
boot_data = self.hd.read(0)
self.memory.load(0, boot_data)
self.cpu.execute()
Der Nutzer des Computer sieht nur start() - nicht die drei Komponenten.
In der Praxis
- jQuery war eine Facade ueber diverse DOM-Unterschiede
- Axios ist eine Facade ueber
XMLHttpRequest/fetch - ORMs wie Prisma sind Facades ueber SQL
Proxy Pattern
Stellvertreter - sieht aus wie das Original, kontrolliert aber den Zugriff:
class Bild:
def __init__(self, dateipfad):
self.dateipfad = dateipfad
self._daten = None
def anzeigen(self):
if self._daten is None:
self._daten = self._laden() # lazy
print(f"Zeige {self.dateipfad}")
def _laden(self):
print(f"Lade {self.dateipfad} (teuer)")
return "..."
class SchutzProxy:
def __init__(self, bild, berechtigt):
self.bild = bild
self.berechtigt = berechtigt
def anzeigen(self):
if not self.berechtigt:
raise PermissionError("Nicht berechtigt")
self.bild.anzeigen()
Proxy-Typen:
- Virtual Proxy: Lazy Loading
- Protection Proxy: Zugriffskontrolle
- Remote Proxy: lokale Stellvertretung fuer Remote-Objekte
- Caching Proxy: Ergebnisse cachen
Composite Pattern
Fuer Baumstrukturen - Einzelteile und Gruppen gleich behandeln:
class DateisystemElement(ABC):
@abstractmethod
def groesse(self): ...
class Datei(DateisystemElement):
def __init__(self, name, groesse):
self.name = name
self._groesse = groesse
def groesse(self): return self._groesse
class Ordner(DateisystemElement):
def __init__(self, name):
self.name = name
self.elemente = []
def hinzufuegen(self, element):
self.elemente.append(element)
def groesse(self):
return sum(e.groesse() for e in self.elemente)
root = Ordner("root")
root.hinzufuegen(Datei("a.txt", 100))
sub = Ordner("sub")
sub.hinzufuegen(Datei("b.txt", 200))
root.hinzufuegen(sub)
print(root.groesse()) # 300
Ordner und Datei haben die gleiche Schnittstelle - rekursive Baumstrukturen werden trivial.
React ist Composite
Eine React-Komponente kann andere enthalten. props.children ist Composite - du behandelst jede Komponente gleich, egal wie komplex ihr Subtree ist.
Zusammenfassung
- Adapter: Interface-Bruecke zwischen inkompatiblen Komponenten
- Decorator: dynamisch Funktionalitaet hinzufuegen
- Facade: einfache Schnittstelle zu komplexem Subsystem
- Proxy: Stellvertreter fuer Access Control, Lazy Loading, Caching
- Composite: Baumstrukturen uniform behandeln
Im naechsten Kapitel: Observer - das wichtigste Behavioral Pattern.