Strategy & Command
Zwei Behavioral Patterns, die Code modular und testbar machen: Strategy fuer austauschbare Algorithmen, Command fuer Aktionen als Objekte.
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.