Strategy Pattern: if-else durch ein Interface ersetzen
1. Überblick
Lange if-else- oder switch-Ketten, um Verhalten je nach Typ auszuwählen, sind ein Zeichen dafür, dass der Code bald schwer zu warten wird. Jedes Mal, wenn du einen neuen Typ hinzufügst, musst du die gleiche Funktion ändern und verletzt damit das Open/Closed Principle. Dieser Artikel zeigt dir, wie du das Strategy Pattern einsetzt, um jedes Verhalten in eine eigene Klasse auszulagern, die ein gemeinsames Interface implementiert, und Spring die passende Strategie automatisch wählen lässt, statt selbst if-else zu schreiben. Nach dem Lesen kannst du einen Service zur Gebühren- oder Rabattberechnung, der momentan unübersichtlich ist, selbst umstrukturieren.
2. Das Problem: if-else-Ketten werden mit der Zeit immer größer
Angenommen, du hast einen PaymentService, der Transaktionsgebühren nach Zahlungsmethode berechnet:
public class PaymentService {
public double calculateFee(String paymentType, double amount) {
if (paymentType.equals("CREDIT_CARD")) {
return amount * 0.03;
} else if (paymentType.equals("BANK_TRANSFER")) {
return amount * 0.01;
} else if (paymentType.equals("E_WALLET")) {
return amount * 0.015;
} else {
throw new IllegalArgumentException("Unsupported type: " + paymentType);
}
}
}
Probleme:
- Um eine neue Zahlungsmethode hinzuzufügen (z. B.
CRYPTO), musst du diese Methode direkt bearbeiten. - Die Gebührenlogik jedes Typs ist in einer Funktion vermischt, was es schwer macht, einzelne Teile zu testen.
- Der Vergleich von
String-Werten führt leicht zu Tippfehlern und es gibt keine Compile-Zeit-Überprüfung.
3. Lösung: ein Strategy-Interface definieren
Die Kernidee: Extrahiere das „Verhalten, das sich je nach Typ ändert” in ein Interface, wobei jeder Typ eine eigene Implementierung ist.
public interface FeeStrategy {
double calculateFee(double amount);
}
Jede Strategie ist jetzt eine eigene Klasse, leicht zu lesen und zu testen:
public class CreditCardFeeStrategy implements FeeStrategy {
@Override
public double calculateFee(double amount) {
return amount * 0.03;
}
}
public class BankTransferFeeStrategy implements FeeStrategy {
@Override
public double calculateFee(double amount) {
return amount * 0.01;
}
}
public class EWalletFeeStrategy implements FeeStrategy {
@Override
public double calculateFee(double amount) {
return amount * 0.015;
}
}
Der PaymentService erhält jetzt nur noch eine FeeStrategy und ruft sie auf, ohne etwas über die innere Logik zu wissen:
public class PaymentService {
private final FeeStrategy feeStrategy;
public PaymentService(FeeStrategy feeStrategy) {
this.feeStrategy = feeStrategy;
}
public double calculateFee(double amount) {
return feeStrategy.calculateFee(amount);
}
}
feeStrategywird via Constructor injiziert,PaymentServicemuss nicht wissen, ob esCreditCardFeeStrategyoder etwas anderes ist.- Eine neue Zahlungsmethode hinzufügen = eine neue Klasse erstellen, die
FeeStrategyimplementiert, ohnePaymentServicezu ändern.
4. Strategy richtig wählen: if-else an anderer Stelle vermeiden
Ein häufiges Problem: du löscht die if-else aus PaymentService, baust sie aber woanders beim Aufrufen des Service wieder auf. Um das zu vermeiden, nutze eine Map<String, FeeStrategy> als Registry und registriere Strategien nach Schlüssel an einer zentralen Stelle (Factory), anstatt if-else überall im Code zu verteilen.
public class FeeStrategyFactory {
private final Map<String, FeeStrategy> strategies;
public FeeStrategyFactory() {
this.strategies = Map.of(
"CREDIT_CARD", new CreditCardFeeStrategy(),
"BANK_TRANSFER", new BankTransferFeeStrategy(),
"E_WALLET", new EWalletFeeStrategy()
);
}
public FeeStrategy resolve(String paymentType) {
FeeStrategy strategy = strategies.get(paymentType);
if (strategy == null) {
throw new IllegalArgumentException("Unsupported type: " + paymentType);
}
return strategy;
}
}
strategiesist eine unveränderbare Map, die einmal beim Initialisieren der Factory gebaut wird.resolve()ist die einzige Stelle, wo noch eine „Bedingung” vorkommt — aber es ist nur ein Map-Lookup, ohne Geschäftslogik. Die echte Logik liegt in jeder Strategie.
Beispiel:
FeeStrategyFactory factory = new FeeStrategyFactory();
FeeStrategy strategy = factory.resolve("E_WALLET");
PaymentService service = new PaymentService(strategy);
double fee = service.calculateFee(1_000_000);
System.out.println(fee);
15000.0
Hinweis: Wenn die Liste der Strategien fest ist und sich selten ändert (2–3 Typen), ist ein einfaches
if-elseimmer noch in Ordnung. Das Strategy Pattern zeigt seinen Wert, wenn die Anzahl der Typen mit der Zeit wächst oder die Logik jedes Typs komplex genug ist, um separat getestet zu werden.
5. Spring nutzen: den Container Strategien automatisch injizieren lassen
In Spring Boot brauchst du die Map nicht manuell zu schreiben. Markiere jede Strategie als @Component, und Spring wird automatisch alle Beans mit dem gleichen Interface in einer List sammeln.
public interface FeeStrategy {
String getPaymentType();
double calculateFee(double amount);
}
Füge getPaymentType() hinzu, damit jede Strategie selbst deklariert, welchen Typ sie bedient:
@Component
public class CreditCardFeeStrategy implements FeeStrategy {
@Override
public String getPaymentType() {
return "CREDIT_CARD";
}
@Override
public double calculateFee(double amount) {
return amount * 0.03;
}
}
Die Factory empfängt nun List<FeeStrategy> von Spring und baut die Map via Constructor:
@Component
public class FeeStrategyFactory {
private final Map<String, FeeStrategy> strategies = new HashMap<>();
public FeeStrategyFactory(List<FeeStrategy> strategyList) {
for (FeeStrategy strategy : strategyList) {
strategies.put(strategy.getPaymentType(), strategy);
}
}
public FeeStrategy resolve(String paymentType) {
FeeStrategy strategy = strategies.get(paymentType);
if (strategy == null) {
throw new IllegalArgumentException("Unsupported type: " + paymentType);
}
return strategy;
}
}
- Spring durchsucht alle Beans, die
FeeStrategyimplementieren, und übergibt sie instrategyListin der Reihenfolge ihrer Initialisierung. - Der Constructor baut die Map genau einmal, wenn
FeeStrategyFactoryerstellt wird — es gibt keinif-elseim gesamten Prozess der Strategiewahl. - Eine neue
CryptoFeeStrategyhinzufügen: erstelle einfach die Klasse, füge@Componenthinzu, und Spring erkennt sie automatisch.FeeStrategyFactorybraucht nicht umgebaut zu werden.
Der PaymentService nutzt diese Factory via @Service:
@Service
public class PaymentService {
private final FeeStrategyFactory feeStrategyFactory;
public PaymentService(FeeStrategyFactory feeStrategyFactory) {
this.feeStrategyFactory = feeStrategyFactory;
}
public double calculateFee(String paymentType, double amount) {
FeeStrategy strategy = feeStrategyFactory.resolve(paymentType);
return strategy.calculateFee(amount);
}
}
6. Tests für jede Strategie und die Factory schreiben
Weil jede Strategie eine unabhängige Klasse ist, kannst du jede einzeln testen, ohne etwas zu mocken:
class CreditCardFeeStrategyTest {
private final CreditCardFeeStrategy strategy = new CreditCardFeeStrategy();
@Test
void shouldCalculate3PercentFee() {
double fee = strategy.calculateFee(1_000_000);
assertThat(fee).isEqualTo(30_000.0);
}
}
Tests für die Factory stellen sicher, dass die Strategiewahl korrekt funktioniert und nicht in falsches if-else-Verhalten fällt:
class FeeStrategyFactoryTest {
private final FeeStrategyFactory factory = new FeeStrategyFactory(
List.of(new CreditCardFeeStrategy(), new EWalletFeeStrategy())
);
@Test
void shouldResolveCorrectStrategy() {
FeeStrategy strategy = factory.resolve("E_WALLET");
assertThat(strategy).isInstanceOf(EWalletFeeStrategy.class);
}
@Test
void shouldThrowWhenTypeNotSupported() {
assertThatThrownBy(() -> factory.resolve("CRYPTO"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("CRYPTO");
}
}
shouldResolveCorrectStrategybestätigt, dass die Factory den richtigen Strategietyp nach Schlüssel zurückgibt.shouldThrowWhenTypeNotSupportedstellt sicher, dass der Fall, wenn eine Strategie nicht gefunden wird, einen klaren Fehler wirft — genau wie das alteelse-Verhalten, aber jetzt konzentriert an einer einzigen Stelle.
7. Enum statt String-Schlüssel verwenden (sicherere Option)
Wenn du dir Sorgen um Tippfehler bei String-Schlüsseln machst, kannst du ein enum als Schlüssel für die Map nutzen, damit der Compiler Fehler früh erkennt:
public enum PaymentType {
CREDIT_CARD, BANK_TRANSFER, E_WALLET
}
public interface FeeStrategy {
PaymentType getPaymentType();
double calculateFee(double amount);
}
Diese Methode behält die gesamte Architektur bei und ändert nur den Schlüsseltyp in der Map<PaymentType, FeeStrategy>. Deine IDE wird Enum-Werte vollständig vorschlagen und hilft dir, Tippfehler bei Strings zu vermeiden.
8. Wann man Strategy Pattern NICHT verwenden sollte
- Es gibt nur 2 Code-Wege, und die Chancen auf Erweiterung sind praktisch null — ein einfaches
if-elseist einfacher zu lesen als 2–3 zusätzliche Klassen zu erstellen. - Die Logik der Wege ist zu 90 % identisch und unterscheidet sich nur in ein paar Parametern — mit Strategy wirst du viele fast identische Klassen erstellen. In diesem Fall ist meist ein
enummit Konfigurationsfeldern (z. B. Gebührenquoten) kompakter. - Das Verhalten wird nicht nach einem „festen Typ” gewählt, sondern hängt von komplexen Runtime-Bedingungen ab (mehrere kombinierte Kriterien) — dann reicht Strategy Pattern allein nicht aus, und du solltest Chain of Responsibility oder eine Rule Engine erwägen.
Hinweis: Wende ein Pattern nicht nur an, weil es „professioneller aussieht”. Nutze es nur, wenn du klar siehst, dass die Anzahl der Code-Wege mit der Zeit wachsen wird oder die Logik jedes Weges komplex genug ist, um separat getestet werden zu müssen.
9. Fazit
Das Strategy Pattern löst genau das Problem langer if-else-Ketten: Es trennt das sich ändernde Verhalten je nach Typ in eigene Klassen, die alle das gleiche Interface implementieren. In Spring Boot nutzt du den Mechanismus des Injizierens von List<Bean>, um Strategien automatisch zu sammeln — ohne die Registry selbst verwalten zu müssen. Das Ergebnis: Neue Typen hinzufügen, ohne alten Code zu verändern, jede Strategie ist einfach isoliert testbar, und es gibt nur noch eine einzige Stelle zum „Wählen einer Strategie” — ein einfacher Map-Lookup, keine if-else mehr im ganzen Code.