Design Patterns

82. Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable; in modern Java, functional interfaces and lambdas make Strategy trivial โ€” Comparator and Predicate are canonical examples.

Java 21 (core language)

Explanation

The Strategy pattern separates what to do from who decides to do it: the context class delegates a behaviour to a strategy object. The strategy is a single-method interface โ€” in Java 8+, this is just a functional interface, and strategies are lambdas or method references.

Classic examples in the JDK: Comparator<T> is a strategy for ordering; Predicate<T> is a strategy for filtering; Runnable is a strategy for a task; Comparator.comparing(...) composes strategies. The pattern appears in every sort, filter, and higher-order function call.

Before Java 8, Strategies required anonymous classes (verbose). Lambdas and functional interfaces make this pattern nearly free โ€” just pass a lambda where a functional interface is expected.

Example

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.function.Predicate;

public class StrategyPattern {
    // Pre-lambda style: explicit strategy interface
    @FunctionalInterface
    interface DiscountStrategy {
        double apply(double price);
    }

    static class ShoppingCart {
        private double total;
        ShoppingCart(double total) { this.total = total; }

        double checkout(DiscountStrategy strategy) {
            return strategy.apply(total);
        }
    }

    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart(100.0);

        // Strategies as lambdas โ€” no class needed!
        DiscountStrategy noDiscount    = price -> price;
        DiscountStrategy tenPercent    = price -> price * 0.90;
        DiscountStrategy flatFive      = price -> price - 5;
        DiscountStrategy loyaltyBonus  = price -> price * 0.80;

        System.out.printf("No discount:   $%.2f%n", cart.checkout(noDiscount));
        System.out.printf("10%% off:       $%.2f%n", cart.checkout(tenPercent));
        System.out.printf("$5 off:        $%.2f%n", cart.checkout(flatFive));
        System.out.printf("Loyalty (20%%): $%.2f%n", cart.checkout(loyaltyBonus));

        // JDK Strategy: Comparator
        List<String> words = Arrays.asList("banana", "apple", "cherry", "kiwi");
        words.sort(Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()));
        System.out.println("Sorted by length: " + words);

        // JDK Strategy: Predicate composition
        Predicate<String> longWord  = s -> s.length() >= 6;
        Predicate<String> startsA   = s -> s.startsWith("a");
        List<String> filtered = words.stream().filter(longWord.or(startsA)).toList();
        System.out.println("Long or starts-a: " + filtered);

        // Runtime strategy selection
        String mode = "loyalty";
        DiscountStrategy selected = switch (mode) {
            case "ten"     -> tenPercent;
            case "flat"    -> flatFive;
            case "loyalty" -> loyaltyBonus;
            default        -> noDiscount;
        };
        System.out.printf("Mode '%s': $%.2f%n", mode, cart.checkout(selected));
    }
}

Sources