Zum Inhalt springen
Design Patterns Fortgeschritten 25 min

Adapter & Decorator

Zwei Structural Patterns im Detail: Adapter fuer Interface-Anpassungen, Decorator fuer das dynamische Erweitern von Verhalten.

Aktualisiert:
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), log4js bridgen 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

SituationInheritanceDecorator
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.

Zurรผck zum Design Patterns Kurs