Description
The Strategy design pattern aims to create a family of related algorithms (behavior) directly linked to a context. In this case, each strategy represents a business/feature that will represent the context according to the strategy.
Situation
🛒 You are developing an e-commerce platform, which means you need a cart with different payment methods. Today, there are several payment methods available on the internet. One solution would be to develop a conditional structure to adapt the user's choice. The problem with this approach is that the code can quickly become unreadable and even impossible to maintain:
- You change one line of code, and it can have an impact on the global behavior.
- A bug occurs, and you must spend a lot of time finding the problem and resolving it.
- You waste time ensuring that your algorithm works for each payment method.
This is where the Strategy pattern comes to the rescue:
- Each payment method is a strategy (separate class) that implements its algorithm.
- The payment context will adapt to the strategy chosen by the user.
Thus, we solve the problems of readability and maintainability.
Advantages and disadvantages
Advantages | Disadvantages |
---|---|
✔️Better code structuring through class decomposition allowing finer segmentation. Thus, if we need to modify strategies, we can modify a class without affecting the context. | ❌Overhead costs in cases where the context creates and initializes parameters that will never be used by it. This case can be avoided by increasing the coupling between strategies and contexts. |
✔️Facilitated maintainability: each class has a specific purpose. In the case of using subclasses via inheritance, the implementation of the algorithm will be mixed with that of the context, which will quickly become difficult to maintain. Thus, adding functionalities to a strategy or fixing a bug becomes much simpler and less risky due to low coupling. | ❌Can create misunderstandings in case of a bad implementation, the choice of a strategy can become complex, or even incomprehensible. This can lead to implementation problems. It should therefore be used only when separate and relevant behaviors can be separated. |
✔️Elimination of conditional structures by creating several classes that represent a context. Otherwise, we would have a context with several conditional structures to determine the right algorithm. | ❌Increase in the number of objects created, since each class represents a strategy, many objects are instantiated. This problem can be countered by coupling the flyweight design pattern with this design pattern to create stateless objects that contexts will share. |
When to use it?
- You have multiple algorithms/behaviors to implement in an object.
- You have multiple classes, the only difference of which is the behavior to be executed.
- You know that the behavior of a method can evolve towards other behaviors.
- You have a lot of conditional structures.
- You want a business logic that does not need to be implemented in this class in the context.
UML diagram
In this example, we are developing a payment system. Here, we know that the site has two payment methods:
- Via your credit card
- Via Paypal
Given this situation, we notice that a strategy pattern can be added.
But why?
Imagine that a new payment method will be added in the future, here we will simply add a new class defining our strategy without impacting the rest of the code. We gain flexibility, but also maintainability.
In this example, our context will be the shoppingCart class. Indeed, this class has a pay() function that takes the Payment interface as a parameter. If we did not use this pattern, we would be faced with using control structures, which would quickly become unreadable in the case of many payment methods.
Implementation
Let's start by creating the basis of our design package: the Payment interface.
Payment
package com.strategy.payment.algorithm;
public interface Payment {
public void pay(int amount);
}
As we can see so far there is nothing complicated. We declare the pay method that will be implemented in the various strategies. To vary the behavior of this method, we will create several strategies. In this method, and for the different strategies, we will display a message in the console to distinguish which strategy is being used.
CreditCardAlgorithm
package com.strategy.payment.algorithm;
public class CreditCardAlgorithm implements Payment {
private String name;
private String cardNumber;
private String card;
private String expirationDate;
public CreditCardAlgorithm(String name, String cardNumber) {
this.name = name;
this.cardNumber = cardNumber;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid with CreditCard");
}
}
PaypalAlgorithm
package com.strategy.payment.algorithm;
public class PaypalAlgorithm implements Payment {
private String email;
private String password;
public PaypalAlgorithm(String email, String password) {
this.email = email;
this.password = password;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid with Paypal");
}
}
Our two algorithms are now defined! As you may have understood, they are very simplified here in order to make the example more understandable. Now that we have our strategies, let's create our context. This context being a "shopping cart", it is important to define a model for our products:
Product
package com.strategy.payment.model;
public class Product {
private int price;
public Product(int price) {
this.price = price;
}
public int getPrice() {
return this.price;
}
}
Now that our model for a product is implemented, we have everything we need to create our shopping cart:
Shopping cart
package com.strategy.payment.context;
import java.util.ArrayList;
import java.util.List;
import com.strategy.payment.algorithm.Payment;
import com.strategy.payment.model.Product;
public class ShoppingCart {
List productList;
public ShoppingCart() {
this.productList = new ArrayList<>();
}
public void addProduct(Product product) {
productList.add(product);
}
public void removeProduct(Product product) {
productList.remove(product);
}
public int calculateTotal() {
int sum = 0;
for (Product product : productList) {
sum += product.getPrice();
}
return sum;
}
public void pay(Payment paymentStrategy) {
int amount = calculateTotal();
paymentStrategy.pay(amount);
}
}
Our context is defined as well as our strategies, now we are going to test this directly in the main method:
Main
package com.strategy.payment;
import com.strategy.payment.algorithm.CreditCardAlgorithm;
import com.strategy.payment.algorithm.PaypalAlgorithm;
import com.strategy.payment.context.ShoppingCart;
import com.strategy.payment.model.Product;
public class Main {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
Product pants = new Product(25);
Product shirt = new Product(15);
cart.addProduct(pants);
cart.addProduct(shirt);
// Payment decisions
cart.pay(new PaypalAlgorithm("email", "12234"));
cart.pay(new CreditCardAlgorithm("Me", "BE844654454545"));
}
}
Result:
40 paid with Paypal
40 paid with CreditCard
Here, we have finished our implementation. What we can notice:
- In case I want to modify the behavior of the
pay
function, I won't disturb the behavior of another strategy. - If a new payment method needs to be implemented, it will be sufficient to add a new class.
- If a bug occurs, it will be easier to investigate in the targeted strategy.
- Here, we have no conditional structure. This means that a client who selects a payment method selects the object of the targeted strategy.
- The algorithms are well separated, in case we have too many classes a pattern can reinforce this pattern (Flyweight).