Zum Inhalt springen
Design Patterns Fortgeschritten 25 min

Strategy & Command

Zwei Behavioral Patterns, die Code modular und testbar machen: Strategy fuer austauschbare Algorithmen, Command fuer Aktionen als Objekte.

Aktualisiert:
Inhaltsverzeichnis

Strategy & Command

Zwei Patterns, die haeufig in modernem Code auftauchen - beide machen Verhalten austauschbar und testbar.

Strategy Pattern

Problem: Ein Algorithmus hat mehrere Varianten, und du willst sie zur Laufzeit austauschen koennen - ohne if-else-Kaskaden.

Loesung: Jede Variante ist eine eigene Strategy-Klasse/Funktion, die die gleiche Schnittstelle implementiert.

Klassisches Beispiel: Sortier-Strategien

from typing import Protocol, Iterable

class SortStrategy(Protocol):
    def sort(self, items: Iterable) -> list: ...

class AlphabetischSort:
    def sort(self, items): return sorted(items)

class LaengeSort:
    def sort(self, items): return sorted(items, key=len)

class UmgekehrtSort:
    def sort(self, items): return sorted(items, reverse=True)

class Sortierer:
    def __init__(self, strategie: SortStrategy):
        self.strategie = strategie

    def sortieren(self, items):
        return self.strategie.sort(items)

namen = ["Anna", "Maximilian", "Leo", "Tim"]

s = Sortierer(AlphabetischSort())
print(s.sortieren(namen))
# ['Anna', 'Leo', 'Maximilian', 'Tim']

s = Sortierer(LaengeSort())
print(s.sortieren(namen))
# ['Leo', 'Tim', 'Anna', 'Maximilian']

Vorteile:

  • Neue Strategien = neue Klasse, kein Aendern von existierendem Code (OCP!)
  • Leicht testbar (Mock-Strategy einsetzen)
  • Klare Verantwortungs-Trennung

Strategy mit Funktionen (pythonisch)

In Python / JavaScript sind Funktionen First-Class - oft reicht eine Funktion statt einer Klasse:

def alphabetisch(items): return sorted(items)
def nach_laenge(items): return sorted(items, key=len)
def umgekehrt(items): return sorted(items, reverse=True)

def sortieren(items, strategie):
    return strategie(items)

print(sortieren(namen, nach_laenge))

Kuerzer, gleich ausdrucksstark.

Zahlungsmethoden - ein weiteres Beispiel

interface PaymentStrategy {
  pay(amount: number): Promise<string>;
}

class CreditCardPayment implements PaymentStrategy {
  async pay(amount: number) {
    // echte Kreditkarten-API...
    return `CC paid ${amount}`;
  }
}

class PayPalPayment implements PaymentStrategy {
  async pay(amount: number) {
    return `PayPal paid ${amount}`;
  }
}

class CryptoPayment implements PaymentStrategy {
  async pay(amount: number) {
    return `Crypto paid ${amount}`;
  }
}

class Checkout {
  constructor(private strategy: PaymentStrategy) {}

  async process(cart: Cart) {
    const total = cart.sum();
    return this.strategy.pay(total);
  }
}

// Nutzer waehlt PayPal
const checkout = new Checkout(new PayPalPayment());
await checkout.process(cart);

Neue Zahlungsmethode? Neue Klasse dazu - der Checkout-Code bleibt unveraendert.

In der Praxis

  • Encoder/Decoder in Libraries (JSON, XML, Protobuf - gleiche Schnittstelle)
  • Validierungen (Email, Phone, Credit-Card - alle als Validator)
  • Rabatt-Regeln in E-Commerce
  • Compression-Algorithmen in File-Handlers

Strategy vs. if-else

Wann lohnt sich Strategy ueberhaupt?

if-else OK

def berechne_rabatt(typ, betrag):
    if typ == "standard":
        return betrag * 0.05
    elif typ == "premium":
        return betrag * 0.10
    return 0

Bei zwei, drei einfachen Varianten - kein Pattern noetig.

Strategy besser

Wenn:

  • Anzahl der Varianten waechst
  • Jede Variante ist komplex (mehrere Methoden, Zustand)
  • Neue Varianten kommen oft dazu
  • Du willst getestet die einzelne Variante koennen

Command Pattern

Problem: Du willst Aktionen als Objekte kapseln - um sie zu queuen, zu loggen, rueckgaengig zu machen, oder spaeter auszufuehren.

Loesung: Jede Aktion ist ein Objekt mit einer execute()-Methode.

Einfaches Beispiel

from abc import ABC, abstractmethod

class Command(ABC):
    @abstractmethod
    def execute(self): ...

class LichtAnCommand(Command):
    def __init__(self, licht):
        self.licht = licht
    def execute(self):
        self.licht.an()

class LichtAusCommand(Command):
    def __init__(self, licht):
        self.licht = licht
    def execute(self):
        self.licht.aus()

class Licht:
    def an(self): print("Licht an")
    def aus(self): print("Licht aus")

# Fernbedienung
class Fernbedienung:
    def __init__(self):
        self.command = None

    def taste_druecken(self):
        self.command.execute()

licht = Licht()
fb = Fernbedienung()

fb.command = LichtAnCommand(licht)
fb.taste_druecken()   # Licht an

fb.command = LichtAusCommand(licht)
fb.taste_druecken()   # Licht aus

Die Fernbedienung kennt keine Lichter - nur Commands. Sehr flexibel.

Undo / Redo

Das Hauptargument fuer Command:

class Editor:
    def __init__(self):
        self.text = ""
        self.history = []

    def execute(self, command):
        command.execute()
        self.history.append(command)

    def undo(self):
        if self.history:
            cmd = self.history.pop()
            cmd.undo()

class TypeCommand(Command):
    def __init__(self, editor, text):
        self.editor = editor
        self.text = text

    def execute(self):
        self.editor.text += self.text

    def undo(self):
        self.editor.text = self.editor.text[:-len(self.text)]

editor = Editor()
editor.execute(TypeCommand(editor, "Hallo, "))
editor.execute(TypeCommand(editor, "Welt!"))
print(editor.text)    # "Hallo, Welt!"

editor.undo()
print(editor.text)    # "Hallo, "
editor.undo()
print(editor.text)    # ""

Das ist die Grundlage von Undo/Redo in Editoren, CAD-Programmen, Grafik-Tools.

Command Queues

Commands koennen in eine Queue - z.B. fuer Job-Scheduling:

from queue import Queue

queue = Queue()

queue.put(EmailCommand("anna@example.com", "Hallo"))
queue.put(DbUpdateCommand(user_id=42, status="active"))
queue.put(CacheCommand("invalidate", key="users"))

# Worker verarbeitet sie im Hintergrund
while not queue.empty():
    cmd = queue.get()
    cmd.execute()

Typisch fuer Background-Jobs, Async-Processing.

Command in JavaScript / React

In React-Pattern wie Redux:

type Action =
  | { type: "INCREMENT" }
  | { type: "DECREMENT" }
  | { type: "SET"; payload: number };

function reducer(state: number, action: Action): number {
  switch (action.type) {
    case "INCREMENT": return state + 1;
    case "DECREMENT": return state - 1;
    case "SET": return action.payload;
  }
}

// Actions sind Commands - jeder hat "type" und optionale "payload"
dispatch({ type: "SET", payload: 42 });

Jede Action ist ein Command-Objekt. Der Reducer ist ein Command-Handler.

Macros - Mehrere Commands zusammen

class MacroCommand(Command):
    def __init__(self, commands):
        self.commands = commands

    def execute(self):
        for c in self.commands:
            c.execute()

    def undo(self):
        for c in reversed(self.commands):
            c.undo()

makro = MacroCommand([
    TypeCommand(editor, "Hallo"),
    TypeCommand(editor, ", "),
    TypeCommand(editor, "Welt!")
])

makro.execute()     # Alle drei ausgefuehrt
makro.undo()        # Alle rueckgaengig (umgekehrte Reihenfolge)

Praktisch fuer Transaktions-Verhalten - alles oder nichts.

State Pattern - verwandter Pattern

Problem: Ein Objekt verhaelt sich unterschiedlich je nach internem Zustand.

Loesung: Jeder Zustand ist eine eigene Klasse.

class State(ABC):
    @abstractmethod
    def play(self): ...
    @abstractmethod
    def pause(self): ...
    @abstractmethod
    def stop(self): ...

class PlayingState(State):
    def play(self): print("Laeuft schon.")
    def pause(self): return PausedState()
    def stop(self): return StoppedState()

class PausedState(State):
    def play(self): return PlayingState()
    def pause(self): print("Schon pausiert.")
    def stop(self): return StoppedState()

class StoppedState(State):
    def play(self): return PlayingState()
    def pause(self): print("Nichts zu pausieren.")
    def stop(self): print("Schon gestoppt.")

class Player:
    def __init__(self):
        self.state = StoppedState()

    def play(self): self.state = self.state.play() or self.state
    def pause(self): self.state = self.state.pause() or self.state
    def stop(self): self.state = self.state.stop() or self.state

Die State-Klassen behandeln jede Methode entsprechend ihrem Zustand - keine grossen if-else-Kaskaden.

Iterator Pattern

Sprach-gestuetzt in Python, JS, Ruby usw. Erlaubt dir, Sammlungen konsistent zu durchlaufen:

class Range:
    def __init__(self, start, stop):
        self.start = start
        self.stop = stop

    def __iter__(self):
        i = self.start
        while i < self.stop:
            yield i
            i += 1

for n in Range(1, 5):
    print(n)   # 1, 2, 3, 4

Das yield macht aus der Funktion einen Generator - einen Iterator.

Sprachen mit Iteratoren als Sprach-Feature (Python, JS, Ruby, C#, Rust) brauchen Iterator als Pattern nicht mehr explizit.

Template Method Pattern

Eine Basis-Klasse definiert den Ablauf, Subklassen uebernehmen die Details:

class DataPipeline(ABC):
    def run(self):
        daten = self.lade()
        verarbeitet = self.verarbeite(daten)
        self.speichere(verarbeitet)

    @abstractmethod
    def lade(self): ...
    @abstractmethod
    def verarbeite(self, daten): ...
    @abstractmethod
    def speichere(self, daten): ...

class CsvPipeline(DataPipeline):
    def lade(self): return pd.read_csv("input.csv")
    def verarbeite(self, daten): return daten.dropna()
    def speichere(self, daten): daten.to_csv("output.csv")

Das run() ist der Template - der feste Ablauf. Die Schritte sind variabel.

Chain of Responsibility

Mehrere Handler bekommen den Request nacheinander - einer fuehlt sich zustaendig, der Rest nicht:

class Handler:
    def __init__(self, next_handler=None):
        self.next_handler = next_handler

    def handle(self, request):
        if self.can_handle(request):
            return self.process(request)
        if self.next_handler:
            return self.next_handler.handle(request)
        return None

    def can_handle(self, r): raise NotImplementedError
    def process(self, r): raise NotImplementedError

class AuthHandler(Handler):
    def can_handle(self, r): return not r.user_authenticated
    def process(self, r): return "403 Forbidden"

class RateLimitHandler(Handler):
    def can_handle(self, r): return r.rate_limit_exceeded
    def process(self, r): return "429 Too Many Requests"

class MainHandler(Handler):
    def can_handle(self, r): return True
    def process(self, r): return f"Hi, {r.user}!"

pipeline = AuthHandler(RateLimitHandler(MainHandler()))

Express Middleware, ASP.NET Middleware, Rack (Ruby) - alle nutzen dieses Pattern.

Zusammenfassung

  • Strategy: austauschbare Algorithmen - in Python/JS oft als Funktionen
  • Command: Aktionen als Objekte fuer Queuing, Undo, Logging
  • State: Verhalten abhaengig vom Zustand - statt if-else-Kaskaden
  • Iterator: einheitlich durchlaufen (Sprach-Feature)
  • Template Method: fester Ablauf, variable Schritte
  • Chain of Responsibility: Request durch Pipeline - Middleware-Pattern

Damit hast du das Pattern-Werkzeug zusammen. Im weiteren Kurs vertiefen wir Architektur-Patterns wie MVC/MVVM, Repository, Event Sourcing und Hexagonal Architecture.

Zurรผck zum Design Patterns Kurs