Skip to content
On this page

Strategy

Defines a series of algorithms, encapsulates them individually, and makes them interchangeable. This pattern allows the algorithms to vary independently of the clients that use them.

The Strategy pattern refers to defining a set of algorithms and encapsulating them in an object. Then, at runtime, one of these algorithms can be used flexibly.

The Strategy pattern is widely used in the Java standard library. For example, let's see how to implement case-insensitive sorting through Arrays.sort():

java
import java.util.Arrays;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        String[] array = { "apple", "Pear", "Banana", "orange" };
        Arrays.sort(array, String::compareToIgnoreCase);
        System.out.println(Arrays.toString(array));
    }
}

If we want to sort ignoring case, we pass in String::compareToIgnoreCase; if we want to sort in reverse order, we pass in (s1, s2) -> -s1.compareTo(s2). The algorithm that compares two elements is the strategy.

In the method Arrays.sort(T[] a, Comparator<? super T> c), the sorting method internally implements the TimSort algorithm. However, the sorting algorithm relies on the Comparator object that we pass in to compare the sizes of two elements. Therefore, the strategy here refers to the method of comparing the sizes of two elements, which can be case-insensitive comparison, reverse comparison, or comparison based on string length.

Thus, the sorting example uses the Strategy pattern, which effectively means that in a method, the process is predetermined, but some key steps rely on the strategy provided by the caller. By passing in different strategies, we can achieve different results, greatly enhancing the system's flexibility.

If we implement the Strategy pattern for sorting using the bubble sort method, it looks as follows:

java
import java.util.*;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        String[] array = { "apple", "Pear", "Banana", "orange" };
        sort(array, String::compareToIgnoreCase);
        System.out.println(Arrays.toString(array));
    }

    static <T> void sort(T[] a, Comparator<? super T> c) {
        for (int i = 0; i < a.length - 1; i++) {
            for (int j = 0; j < a.length - 1 - i; j++) {
                if (c.compare(a[j], a[j + 1]) > 0) { // Note that comparing two elements' sizes depends on the strategy passed in
                    T temp = a[j];
                    a[j] = a[j + 1];
                    a[j + 1] = temp;
                }
            }
        }
    }
}

A complete Strategy pattern involves defining the strategy and the context in which the strategy is used. Taking a shopping cart checkout example, suppose the website has different discounts for regular and Prime members, and during promotions, there is also an offer for a discount of 20 for purchases over 100. These can be implemented as strategies. First, we define the discount strategy interface:

java
public interface DiscountStrategy {
    // Calculate discount amount:
    BigDecimal getDiscount(BigDecimal total);
}

Next, we implement various strategies. The regular user strategy is as follows:

java
public class UserDiscountStrategy implements DiscountStrategy {
    public BigDecimal getDiscount(BigDecimal total) {
        // Regular member gets a 10% discount:
        return total.multiply(new BigDecimal("0.1")).setScale(2, RoundingMode.DOWN);
    }
}

The over-discount strategy is as follows:

java
public class OverDiscountStrategy implements DiscountStrategy {
    public BigDecimal getDiscount(BigDecimal total) {
        // Discount of 20 for purchases over 100:
        return total.compareTo(BigDecimal.valueOf(100)) >= 0 ? BigDecimal.valueOf(20) : BigDecimal.ZERO;
    }
}

Finally, to apply the strategy, we need a DiscountContext:

java
public class DiscountContext {
    // Holds a specific strategy:
    private DiscountStrategy strategy = new UserDiscountStrategy();

    // Allows the client to set a new strategy:
    public void setStrategy(DiscountStrategy strategy) {
        this.strategy = strategy;
    }

    public BigDecimal calculatePrice(BigDecimal total) {
        return total.subtract(this.strategy.getDiscount(total)).setScale(2);
    }
}

The caller must first create a DiscountContext and specify a strategy (or use the default strategy) to obtain the price after the discount:

java
DiscountContext ctx = new DiscountContext();

// Default uses the regular member discount:
BigDecimal pay1 = ctx.calculatePrice(BigDecimal.valueOf(105));
System.out.println(pay1);

// Use the over-discount strategy:
ctx.setStrategy(new OverDiscountStrategy());
BigDecimal pay2 = ctx.calculatePrice(BigDecimal.valueOf(105));
System.out.println(pay2);

// Use the Prime member discount:
ctx.setStrategy(new PrimeDiscountStrategy());
BigDecimal pay3 = ctx.calculatePrice(BigDecimal.valueOf(105));
System.out.println(pay3);

The complete Strategy pattern is illustrated in the following diagram:

┌───────────────┐      ┌─────────────────┐
│DiscountContext│──────▶│DiscountStrategy │
└───────────────┘      └─────────────────┘

                                │ ┌─────────────────────┐
                                ├─│UserDiscountStrategy │
                                │ └─────────────────────┘
                                │ ┌─────────────────────┐
                                ├─│PrimeDiscountStrategy│
                                │ └─────────────────────┘
                                │ ┌─────────────────────┐
                                └─│OverDiscountStrategy │
                                  └─────────────────────┘

The core idea of the Strategy pattern is to extract easily changeable algorithms as "strategy" parameters passed into a computation method, allowing new strategies to be added without modifying the existing logic.

Practice

Use the Strategy pattern to add a new strategy that allows an additional 30% discount on top of the 20 discount for Prime members when spending over 100.

Summary

The Strategy pattern allows the caller to choose an algorithm, enabling different calculations through various strategies. By extending strategies, new results can be achieved without modifying the main logic.

Strategy has loaded