Design patterns are very popular among software engineers. However, only a few of them prove truly useful when dealing with real-world projects and commonly used frameworks and libraries.
In this post, we will dive deep into the Strategy Design Pattern and explore how to leverage its functionality in real-world applications.
What is the Strategy Design Pattern?
The Strategy Design Pattern is a behavioral design pattern that allows you to define a family of algorithms, encapsulate each one in a separate class, and make their objects interchangeable.
The key characteristics of this design pattern are:
- Avoids excessive use of if-else statements by encapsulating different behaviors in separate classes.
- Easily extendable — to modify the existing code, you just need to add a new strategy.
- Promotes the Open/Closed Principle, making the code open for extension but closed for modification.
A common example used to illustrate this pattern is a payment system. Consider this scenario:
You are building a payment system where users can pay using different methods such as Credit Card, PayPal, or Cryptocurrency. Each payment method has different processing logic. If you write all of them inside a single class, your code will become messy and difficult to maintain.
Instead, you:
- Create an interface that defines a common method, e.g.,
pay(double amount). - Implement separate classes for each payment method, each following that interface.
- Use each class interchangeably depending on the use case.
public interface PaymentStrategy {
void pay(double amount);
}
public class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " using Credit Card.");
}
}
public class PayPalPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " using PayPal.");
}
}
public class CryptoPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " using Cryptocurrency.");
}
}That's cool! But in real applications, most of the time we don't manually choose which strategy our code will use — we need this to be determined dynamically at runtime based on certain conditions.
Using the Strategy Design Pattern in the Quarkus Framework
Now, let's see how we can leverage this design pattern in a Quarkus-based application, making our implementation capable of switching strategies dynamically at runtime.
Imagine the payment method is determined when an HTTP request is made to a REST endpoint:
public record PaymentRequest(
double amount,
String paymentMethod
) {}First, we extend the PaymentStrategy interface by adding a method that identifies each strategy by name:
public interface PaymentStrategy {
String name();
void pay(double amount);
}Each implementation must provide a unique name():
@ApplicationScoped
public class CreditCardPayment implements PaymentStrategy {
@Override
public String name() { return "Credit card"; }
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " using Credit Card.");
}
}
@ApplicationScoped
public class PayPalPayment implements PaymentStrategy {
@Override
public String name() { return "PayPal"; }
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " using PayPal.");
}
}
@ApplicationScoped
public class CryptoPayment implements PaymentStrategy {
@Override
public String name() { return "Crypto"; }
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " using Cryptocurrency.");
}
}Next, we inject all implementations into a PaymentService that dispatches based on the request:
@ApplicationScoped
public class PaymentService {
private final Map<String, PaymentStrategy> paymentStrategiesByName;
public PaymentService(Instance<PaymentStrategy> paymentStrategies) {
this.paymentStrategiesByName = paymentStrategies.stream()
.collect(Collectors.toMap(PaymentStrategy::name, Function.identity()));
}
public void pay(PaymentRequest paymentRequest) {
var paymentStrategy = Optional
.ofNullable(paymentStrategiesByName.get(paymentRequest.paymentMethod()))
.orElseThrow(() -> new UnknownPaymentMethodException(
"Unknown payment method: " + paymentRequest.paymentMethod()));
paymentStrategy.pay(paymentRequest.amount());
}
}With this implementation, we have built an enhanced Strategy Design Pattern for real-world applications. This example was implemented using Quarkus, but the same approach applies equally well in Spring Boot or Micronaut.