Design Patterns sind bewährte Lösungsschablonen für wiederkehrende Entwurfsprobleme in der Softwareentwicklung. Sie werden klassisch in drei Kategorien eingeteilt:
Kapseln die Objekterzeugung, sodass der Client nicht direkt von konkreten Klassen oder dem new-Aufruf abhängt.
Setzen Objekte/Klassen zu größeren, flexiblen Strukturen zusammen, ohne deren Kern zu verändern.
Regeln Kommunikation und Verantwortlichkeiten zwischen Objekten.
Zweck: Trennt die schrittweise Konstruktion eines komplexen Objekts von seiner Repräsentation, sodass derselbe Konstruktionsprozess unterschiedliche Repräsentationen erzeugen kann.
Problem: Konstruktoren mit vielen (optionalen) Parametern werden unübersichtlich ("Telescoping Constructor").
Lösung: Ein separater Builder-Objekt erhält die Konstruktionsschritte einzeln (fluent API) und liefert am Ende das fertige Objekt.
Beispiel:
Pizza pizza = new Pizza.Builder()
.size(32)
.cheese(true)
.pepperoni(true)
.build();
Vor-/Nachteile: Lesbare, flexible Objekterzeugung; erfordert aber zusätzliche Builder-Klasse.
Zweck: Macht Schnittstellen kompatibel, die sonst nicht zusammenpassen würden ("Übersetzer" zwischen zwei Klassen).
Problem: Eine bestehende Klasse soll mit einer inkompatiblen Schnittstelle verwendet werden, ohne ihren Quellcode zu ändern.
Lösung: Eine Wrapper-Klasse implementiert die erwartete Schnittstelle und delegiert die Aufrufe intern an die inkompatible Klasse.
Beispiel:
interface MediaPlayer { void play(String file); }
class LegacyPlayer { void playOldFormat(String file) { /* ... */ } }
class MediaAdapter implements MediaPlayer {
private LegacyPlayer legacy = new LegacyPlayer();
public void play(String file) { legacy.playOldFormat(file); }
}
Vor-/Nachteile: Erlaubt Wiederverwendung von Altcode; führt aber eine zusätzliche Indirektionsschicht ein.
Zweck: Fügt einem Objekt dynamisch zusätzliches Verhalten hinzu, ohne seine Klasse zu verändern (Alternative zur Vererbung).
Problem: Neue Funktionalität soll flexibel kombinierbar sein, ohne für jede Kombination eine eigene Unterklasse zu erzeugen.
Lösung: Decorator-Objekte implementieren dieselbe Schnittstelle wie das Basisobjekt, umschließen es und erweitern dessen Verhalten.
Beispiel:
interface Coffee { double cost(); }
class SimpleCoffee implements Coffee { public double cost() { return 2.0; } }
class MilkDecorator implements Coffee {
private Coffee coffee;
MilkDecorator(Coffee c) { this.coffee = c; }
public double cost() { return coffee.cost() + 0.5; }
}
Coffee order = new MilkDecorator(new SimpleCoffee());
Vor-/Nachteile: Flexibler als statische Vererbung; kann aber zu vielen kleinen, verschachtelten Objekten führen.
Zweck: Bietet eine vereinfachte, einheitliche Schnittstelle für ein komplexes Subsystem aus vielen Klassen.
Problem: Client-Code muss sonst direkt mit vielen Unterklassen eines Subsystems interagieren, was hohe Kopplung erzeugt.
Lösung: Eine Facade-Klasse kapselt die Komplexität und bietet einfache High-Level-Methoden an.
Beispiel:
class VideoConverterFacade {
public File convert(String filename, String format) {
VideoFile file = new VideoFile(filename);
Codec codec = CodecFactory.extract(file);
// weitere interne Schritte ...
return new BitrateReader().convert(file, codec);
}
}
Vor-/Nachteile: Vereinfacht die Nutzung und reduziert Kopplung; kann sich zu einem "God Object" entwickeln, wenn zu viel Logik hineinwandert.
Zweck: Stellt ein Stellvertreterobjekt bereit, das den Zugriff auf ein anderes Objekt kontrolliert (z. B. Lazy Loading, Zugriffskontrolle, Logging).
Problem: Direkter Zugriff auf ein Objekt ist teuer, unsicher oder soll um zusätzliches Verhalten ergänzt werden.
Lösung: Der Proxy implementiert dieselbe Schnittstelle wie das reale Objekt und leitet Aufrufe kontrolliert weiter.
Beispiel:
interface Image { void display(); }
class RealImage implements Image {
RealImage(String file) { loadFromDisk(file); }
public void display() { /* zeigt Bild an */ }
private void loadFromDisk(String file) { /* teuer */ }
}
class ProxyImage implements Image {
private RealImage realImage;
private String filename;
ProxyImage(String filename) { this.filename = filename; }
public void display() {
if (realImage == null) realImage = new RealImage(filename);
realImage.display();
}
}
Vor-/Nachteile: Ermöglicht Lazy Loading und Zugriffskontrolle; fügt zusätzliche Indirektion und Komplexität hinzu.
Zweck: Fasst Objekte zu Baumstrukturen zusammen und erlaubt es, Einzelobjekte und Objektgruppen einheitlich zu behandeln.
Problem: Client-Code muss zwischen einzelnen Objekten und Zusammensetzungen von Objekten unterscheiden, obwohl beide gleich behandelt werden sollen (z. B. Datei vs. Ordner).
Lösung: Eine gemeinsame Schnittstelle für Blatt- und Kompositobjekte, wobei das Kompositobjekt Operationen an seine Kinder delegiert.
Beispiel:
interface FileSystemItem { int getSize(); }
class File implements FileSystemItem {
private int size;
public int getSize() { return size; }
}
class Folder implements FileSystemItem {
private List<FileSystemItem> children = new ArrayList<>();
public int getSize() {
return children.stream().mapToInt(FileSystemItem::getSize).sum();
}
}
Vor-/Nachteile: Vereinfacht den Umgang mit Baumstrukturen; kann das Design zu allgemein machen, wenn Blatt- und Kompositklassen stark unterschiedliche Verantwortlichkeiten haben.
Zweck: Kapselt austauschbare Algorithmen in separaten Klassen und macht sie zur Laufzeit austauschbar.
Problem: Eine Klasse soll zwischen mehreren Varianten eines Algorithmus wählen können, ohne dass dies zu großen bedingten Verzweigungen (if/switch) führt.
Lösung: Jeder Algorithmus wird als eigene Klasse hinter einer gemeinsamen Schnittstelle implementiert; der Kontext hält eine Referenz auf die aktuell gewählte Strategie.
Beispiel:
interface PaymentStrategy { void pay(double amount); }
class CreditCardPayment implements PaymentStrategy {
public void pay(double amount) { /* ... */ }
}
class ShoppingCart {
private PaymentStrategy strategy;
void setStrategy(PaymentStrategy s) { this.strategy = s; }
void checkout(double amount) { strategy.pay(amount); }
}
Vor-/Nachteile: Vermeidet umfangreiche Verzweigungslogik und macht Verhalten austauschbar; erhöht die Klassenanzahl.
Zweck: Kapselt eine Anfrage bzw. Aktion als eigenständiges Objekt, wodurch sich Aufrufer und Ausführender entkoppeln lassen (ermöglicht u. a. Undo/Redo, Warteschlangen, Logging).
Problem: Ein Sender soll eine Aktion auslösen können, ohne den konkreten Empfänger oder die Ausführungsdetails zu kennen.
Lösung: Jede Aktion wird als Command-Objekt mit einer execute()-Methode gekapselt; der Aufrufer kennt nur die Command-Schnittstelle.
Beispiel:
interface Command { void execute(); }
class LightOnCommand implements Command {
private Light light;
LightOnCommand(Light light) { this.light = light; }
public void execute() { light.on(); }
}
class RemoteControl {
private Command command;
void setCommand(Command c) { this.command = c; }
void pressButton() { command.execute(); }
}
Vor-/Nachteile: Entkoppelt Aufrufer und Empfänger, ermöglicht Undo/Redo und Queuing; führt zu vielen kleinen Command-Klassen.
Zweck: Bietet eine Möglichkeit, sequenziell auf die Elemente einer Sammlung zuzugreifen, ohne deren interne Struktur offenzulegen.
Problem: Verschiedene Collection-Typen (Liste, Baum, Menge) sollen einheitlich durchlaufen werden können.
Lösung: Ein Iterator-Objekt kapselt den Zustand des Durchlaufs und stellt Methoden wie hasNext()/next() bereit.
Beispiel:
interface Iterator<T> { boolean hasNext(); T next(); }
class ListIterator<T> implements Iterator<T> {
private List<T> items;
private int position = 0;
ListIterator(List<T> items) { this.items = items; }
public boolean hasNext() { return position < items.size(); }
public T next() { return items.get(position++); }
}
Vor-/Nachteile: Vereinheitlicht den Zugriff auf unterschiedliche Datenstrukturen; zusätzlicher Overhead bei sehr einfachen Sammlungen.
Zweck: Erlaubt einem Objekt, sein Verhalten zu ändern, wenn sich sein interner Zustand ändert – es wirkt so, als würde die Klasse des Objekts wechseln.
Problem: Zustandsabhängiges Verhalten wird oft durch große if/switch-Blöcke abgebildet, die schwer wartbar sind.
Lösung: Jeder Zustand wird als eigene Klasse mit einer gemeinsamen Schnittstelle implementiert; der Kontext delegiert Aufrufe an das aktuelle State-Objekt.
Beispiel:
interface State { void handle(OrderContext context); }
class PendingState implements State {
public void handle(OrderContext context) {
context.setState(new ShippedState());
}
}
class OrderContext {
private State state = new PendingState();
void setState(State s) { this.state = s; }
void next() { state.handle(this); }
}
Vor-/Nachteile: Vermeidet komplexe Zustandsbedingungen und macht Übergänge explizit; erhöht die Klassenanzahl.
Zweck: Definiert das Skelett eines Algorithmus in einer Basisklasse und überlässt einzelne Schritte den Unterklassen zur Implementierung.
Problem: Mehrere Klassen implementieren einen ähnlichen Algorithmus mit leicht unterschiedlichen Einzelschritten, was zu Codeduplikation führt.
Lösung: Die Basisklasse definiert eine finale Methode mit dem festen Ablauf, die abstrakte oder überschreibbare Hook-Methoden aufruft.
Beispiel:
abstract class DataProcessor {
final void process() {
readData();
transformData();
saveData();
}
abstract void readData();
abstract void transformData();
void saveData() { /* Standardimplementierung */ }
}
class CsvProcessor extends DataProcessor {
void readData() { /* CSV lesen */ }
void transformData() { /* CSV-spezifisch transformieren */ }
}
Vor-/Nachteile: Reduziert Codeduplikation und erzwingt einen einheitlichen Ablauf; Vererbung schränkt Flexibilität stärker ein als Komposition.
Zweck: Gibt eine Anfrage entlang einer Kette von potenziellen Bearbeitern weiter, bis einer von ihnen sie behandelt.
Problem: Mehrere Objekte könnten eine Anfrage bearbeiten, der Sender soll den konkreten Empfänger aber nicht kennen müssen.
Lösung: Jeder Handler erhält eine Referenz auf den nächsten Handler in der Kette und entscheidet, ob er die Anfrage selbst bearbeitet oder weiterreicht.
Beispiel:
abstract class Handler {
protected Handler next;
void setNext(Handler next) { this.next = next; }
abstract void handle(Request request);
}
class AuthHandler extends Handler {
void handle(Request request) {
if (!request.isAuthenticated()) {
System.out.println("Zugriff verweigert");
return;
}
if (next != null) next.handle(request);
}
}
Vor-/Nachteile: Entkoppelt Sender und Empfänger, flexible Kettenkonfiguration; keine Garantie, dass eine Anfrage überhaupt behandelt wird.