Skill „Entwurfsmuster“
Praktische Referenz zu Entwurfsmustern (Design Patterns) für Java mit modernen Beispielen.
Verwendung
- Der Benutzer bittet um die Implementierung eines bestimmten Musters
- Entwurf erweiterbarer/flexibler Komponenten
- Refactoring von starren Codestrukturen
- Code-Review schlägt die Verwendung eines Musters vor
Kurzreferenz: Wann verwendet man was
| Problem | Muster |
|---|---|
| Komplexe Objektkonstruktion | Builder |
| Objekte ohne Angabe der Klasse erstellen | Factory |
| Mehrere Algorithmen, zur Laufzeit austauschbar | Strategy |
| Verhalten hinzufügen, ohne die Klasse zu ändern | Decorator |
| Mehrere Objekte über Änderungen benachrichtigen | Observer |
| Einzelne Instanz sicherstellen | Singleton |
| Inkompatible Schnittstellen konvertieren | Adapter |
| Algorithmusskelett definieren | Template Method |
Erzeugungsmuster
Builder
Verwendung: Objekt hat viele Parameter, einige optional.
// ❌ Teleskopierendes Konstruktor-Anti-Pattern
public class User {
public User(String name) { }
public User(String name, String email) { }
public User(String name, String email, int age) { }
public User(String name, String email, int age, String phone) { }
// ... Explosion von Konstruktoren
}
// ✅ Builder-Pattern
public class User {
private final String name; // erforderlich
private final String email; // erforderlich
private final int age; // optional
private final String phone; // optional
private final String address; // optional
private User(Builder builder) {
this.name = builder.name;
this.email = builder.email;
this.age = builder.age;
this.phone = builder.phone;
this.address = builder.address;
}
public static Builder builder(String name, String email) {
return new Builder(name, email);
}
public static class Builder {
// Erforderlich
private final String name;
private final String email;
// Optional mit Standardwerten
private int age = 0;
private String phone = "";
private String address = "";
private Builder(String name, String email) {
this.name = name;
this.email = email;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Builder phone(String phone) {
this.phone = phone;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public User build() {
return new User(this);
}
}
}
// Verwendung
User user = User.builder("John", "[email protected]")
.age(30)
.phone("+1234567890")
.build();
Mit Lombok:
@Builder
@Getter
public class User {
private final String name;
private final String email;
@Builder.Default private int age = 0;
private String phone;
}
Factory Method
Verwendung: Objekte erstellen, ohne die genaue Klasse anzugeben.
// ✅ Factory-Method-Pattern
public interface Notification {
void send(String message);
}
public class EmailNotification implements Notification {
@Override
public void send(String message) {
System.out.println("E-Mail: " + message);
}
}
public class SmsNotification implements Notification {
@Override
public void send(String message) {
System.out.println("SMS: " + message);
}
}
public class PushNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Push: " + message);
}
}
// Factory
public class NotificationFactory {
public static Notification create(String type) {
return switch (type.toUpperCase()) {
case "EMAIL" -> new EmailNotification();
case "SMS" -> new SmsNotification();
case "PUSH" -> new PushNotification();
default -> throw new IllegalArgumentException("Unbekannter Typ: " + type);
};
}
}
// Verwendung
Notification notification = NotificationFactory.create("EMAIL");
notification.send("Hallo!");
Mit Spring (bevorzugt):
public interface NotificationSender {
void send(String message);
String getType();
}
@Component
public class EmailSender implements NotificationSender {
@Override public void send(String message) { /* ... */ }
@Override public String getType() { return "EMAIL"; }
}
@Component
public class SmsSender implements NotificationSender {
@Override public void send(String message) { /* ... */ }
@Override public String getType() { return "SMS"; }
}
@Component
public class NotificationFactory {
private final Map<String, NotificationSender> senders;
public NotificationFactory(List<NotificationSender> senderList) {
this.senders = senderList.stream()
.collect(Collectors.toMap(
NotificationSender::getType,
Function.identity()
));
}
public NotificationSender getSender(String type) {
return Optional.ofNullable(senders.get(type))
.orElseThrow(() -> new IllegalArgumentException("Unbekannt: " + type));
}
}
Singleton
Verwendung: Genau eine Instanz wird benötigt (sparsam verwenden!).
// ✅ Modernes Singleton (enum-basiert, threadsicher)
public enum DatabaseConnection {
INSTANCE;
private Connection connection;
DatabaseConnection() {
// Verbindung initialisieren
}
public Connection getConnection() {
return connection;
}
}
// Verwendung
Connection conn = DatabaseConnection.INSTANCE.getConnection();
Mit Spring (bevorzugt):
@Component // Standard-Scope ist Singleton
public class DatabaseConnection {
// Spring verwaltet die einzelne Instanz
}
Warnung: Singletons können problematisch sein:
- Schwer zu testen (globaler Zustand)
- Versteckte Abhängigkeiten
- Stattdessen Abhängigkeitsinjektion in Betracht ziehen
Verhaltensmuster
Strategy
Verwendung: Mehrere Algorithmen für denselben Vorgang, zur Laufzeit austauschbar.
// ✅ Strategy-Pattern
public interface PaymentStrategy {
void pay(BigDecimal amount);
}
public class CreditCardPayment implements PaymentStrategy {
private final String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public void pay(BigDecimal amount) {
System.out.println("Bezahlt " + amount + " mit Karte " + cardNumber);
}
}
public class PayPalPayment implements PaymentStrategy {
private final String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(BigDecimal amount) {
System.out.println("Bezahlt " + amount + " via PayPal: " + email);
}
}
public class CryptoPayment implements PaymentStrategy {
private final String walletAddress;
public CryptoPayment(String walletAddress) {
this.walletAddress = walletAddress;
}
@Override
public void pay(BigDecimal amount) {
System.out.println("Bezahlt " + amount + " an Wallet: " + walletAddress);
}
}
// Kontext
public class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(BigDecimal total) {
paymentStrategy.pay(total);
}
}
// Verwendung
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new CreditCardPayment("4111-1111-1111-1111"));
cart.checkout(new BigDecimal("99.99"));
// Strategie zur Laufzeit ändern
cart.setPaymentStrategy(new PayPalPayment("[email protected]"));
cart.checkout(new BigDecimal("49.99"));
Mit Java 8+ (funktional):
// Strategie als funktionales Interface
@FunctionalInterface
public interface PaymentStrategy {
void pay(BigDecimal amount);
}
// Verwendung mit Lambdas
PaymentStrategy creditCard = amount ->
System.out.println("Kartenzahlung: " + amount);
PaymentStrategy paypal = amount ->
System.out.println("PayPal-Zahlung: " + amount);
cart.setPaymentStrategy(creditCard);
Observer
Verwendung: Objekte müssen über Änderungen an einem anderen Objekt benachrichtigt werden.
// ✅ Observer-Pattern (modernes Java)
public interface OrderObserver {
void onOrderPlaced(Order order);
}
public class OrderService {
private final List<OrderObserver> observers = new ArrayList<>();
public void addObserver(OrderObserver observer) {
observers.add(observer);
}
public void removeObserver(OrderObserver observer) {
observers.remove(observer);
}
public void placeOrder(Order order) {
// Bestellung verarbeiten
saveOrder(order);
// Alle Beobachter benachrichtigen
observers.forEach(observer -> observer.onOrderPlaced(order));
}
}
// Beobachter
public class InventoryService implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
// Lagerbestand reduzieren
order.getItems().forEach(item ->
reduceStock(item.getProductId(), item.getQuantity())
);
}
}
public class EmailNotificationService implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
sendConfirmationEmail(order.getCustomerEmail(), order);
}
}
public class AnalyticsService implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
trackOrderEvent(order);
}
}
// Einrichtung
OrderService orderService = new OrderService();
orderService.addObserver(new InventoryService());
orderService.addObserver(new EmailNotificationService());
orderService.addObserver(new AnalyticsService());
Mit Spring Events (bevorzugt):
// Event
public record OrderPlacedEvent(Order order) {}
// Publisher
@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
public void placeOrder(Order order) {
saveOrder(order);
eventPublisher.publishEvent(new OrderPlacedEvent(order));
}
}
// Listener (Beobachter)
@Component
public class InventoryListener {
@EventListener
public void handleOrderPlaced(OrderPlacedEvent event) {
// Lagerbestand reduzieren
}
}
@Component
public class EmailListener {
@EventListener
public void handleOrderPlaced(OrderPlacedEvent event) {
// E-Mail senden
}
@EventListener
@Async // Asynchrone Verarbeitung
public void handleOrderPlacedAsync(OrderPlacedEvent event) {
// E-Mail asynchron senden
}
}
Template Method
Verwendung: Algorithmusskelett definieren, Unterklassen füllen die Schritte aus.
// ✅ Template-Method-Pattern
public abstract class DataProcessor {
// Template-Methode - definiert den Algorithmus
public final void process() {
readData();
processData();
writeData();
if (shouldNotify()) {
notifyCompletion();
}
}
// Schritte, die von Unterklassen implementiert werden
protected abstract void readData();
protected abstract void processData();
protected abstract void writeData();
// Hook - optionale Überschreibung
protected boolean shouldNotify() {
return true;
}
protected void notifyCompletion() {
System.out.println("Verarbeitung abgeschlossen!");
}
}
public class CsvDataProcessor extends DataProcessor {
@Override
protected void readData() {
System.out.println("Lese CSV-Datei...");
}
@Override
protected void processData() {
System.out.println("Verarbeite CSV-Daten...");
}
@Override
protected void writeData() {
System.out.println("Schreibe in Datenbank...");
}
}
public class ApiDataProcessor extends DataProcessor {
@Override
protected void readData() {
System.out.println("Rufe von API ab...");
}
@Override
protected void processData() {
System.out.println("Transformiere API-Antwort...");
}
@Override
protected void writeData() {
System.out.println("Schreibe in Cache...");
}
@Override
protected boolean shouldNotify() {
return false; // Hook überschreiben
}
}
// Verwendung
DataProcessor csvProcessor = new CsvDataProcessor();
csvProcessor.process();
DataProcessor apiProcessor = new ApiDataProcessor();
apiProcessor.process();
Strukturmuster
Decorator
Verwendung: Verhalten dynamisch hinzufügen, ohne vorhandene Klassen zu ändern.
// ✅ Decorator-Pattern
public interface Coffee {
String getDescription();
BigDecimal getCost();
}
public class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Kaffee";
}
@Override
public BigDecimal getCost() {
return new BigDecimal("2.00");
}
}
// Basis-Dekorator
public abstract class CoffeeDecorator implements Coffee {
protected final Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public String getDescription() {
return coffee.getDescription();
}
@Override
public BigDecimal getCost() {
return coffee.getCost();
}
}
// Konkrete Dekoratoren
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Milch";
}
@Override
public BigDecimal getCost() {
return coffee.getCost().add(new BigDecimal("0.50"));
}
}
public class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Zucker";
}
@Override
public BigDecimal getCost() {
return coffee.getCost().add(new BigDecimal("0.20"));
}
}
public class WhippedCreamDecorator extends CoffeeDecorator {
public WhippedCreamDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Schlagsahne";
}
@Override
public BigDecimal getCost() {
return coffee.getCost().add(new BigDecimal("0.70"));
}
}
// Verwendung
Coffee coffee = new SimpleCoffee();
System.out.println(coffee.getDescription() + " kostet " + coffee.getCost());
Coffee milkCoffee = new MilkDecorator(new SimpleCoffee());
System.out.println(milkCoffee.getDescription() + " kostet " + milkCoffee.getCost());
Coffee fancyCoffee = new WhippedCreamDecorator(new SugarDecorator(new MilkDecorator(new SimpleCoffee())));
System.out.println(fancyCoffee.getDescription() + " kostet " + fancyCoffee.getCost());
Adapter
Verwendung: Zwei inkompatible Schnittstellen zusammenbringen.
// ✅ Adapter-Pattern
// Angenommen, ein externer Dienst verwendet ein Legacy-Format
public class LegacyUserService {
public String getUserXml(int userId) {
return "<user><id>" + userId + "</id></user>";
}
}
// Moderne Schnittstelle, die wir verwenden möchten
public interface ModernUserService {
User getUser(int userId);
}
public record User(int id, String name, String email) {}
// Adapter
public class UserServiceAdapter implements ModernUserService {
private final LegacyUserService legacyService;
private final XmlParser xmlParser;
public UserServiceAdapter(LegacyUserService legacyService, XmlParser xmlParser) {
this.legacyService = legacyService;
this.xmlParser = xmlParser;
}
@Override
public User getUser(int userId) {
String xml = legacyService.getUserXml(userId);
return xmlParser.parseUser(xml);
}
}
// Verwendung
LegacyUserService legacyUserService = new LegacyUserService();
ModernUserService userService = new UserServiceAdapter(legacyUserService, new XmlParser());
User user = userService.getUser(123);
Best Practices
Wann verwenden
- Entwurfsmuster sind Lösungen für wiederkehrende Probleme, nicht Ziele
- Überspringe ein Muster, wenn es die Komplexität unnötig erhöht
- Ziehe funktionale Ansätze (Lambdas, Streams) einfachen OOP-Mustern vor
- Spring und andere Frameworks bieten integrierte "Pattern"-Unterstützung
Anti-Patterns zu vermeiden
- Singleton-Überbeanspruchung: Ein Singleton pro Anwendung ist akzeptabel, ein Singleton pro Klasse nicht
- Factory-Überbeanspruchung: Erstelle keine Factory, wenn es nur eine Implementierung gibt
- Muster auswendig lernen: Verstehe das Problem, nicht nur das Muster
- Komplexität um der Komplexität willen: Manchmal ist einfacher Code besser
Moderne Alternativen
- Strategy -> Lambda-Ausdrücke und Funktionsreferenzen
- Observer -> Reactive Streams, Spring Events
- Decorator -> Zusammensetzung mit Streams/bibliotheksspezifischen Mitteln
- Factory -> Spring Beans + Dependency Injection
- Builder -> Lombok
@Builder, Records mit benannten Argumenten (falls unterstützt)