How to Implement the Strategy Pattern for Runtime Algorithm Selection in Java
The Strategy pattern enables runtime algorithm selection by encapsulating interchangeable algorithms behind a common interface and delegating execution to a context class that can switch implementations dynamically without modifying its own code.
The Strategy pattern is a behavioral design pattern that defines a family of algorithms, makes them interchangeable, and allows the algorithm to vary independently from clients that use it. According to the iluwatar/java-design-patterns repository, this pattern is implemented in the strategy module using a dragon-slaying metaphor where different combat algorithms can be swapped at runtime. The implementation demonstrates both classic object-oriented approaches and modern Java 8 functional techniques.
Core Architecture Components
The Strategy pattern implementation in the repository consists of four primary components that work together to enable flexible algorithm selection.
The Strategy Interface
At the heart of the pattern is the DragonSlayingStrategy interface defined in strategy/src/main/java/com/iluwatar/strategy/DragonSlayingStrategy.java. This functional interface declares the contract that all concrete algorithms must fulfill:
@FunctionalInterface
public interface DragonSlayingStrategy {
void execute();
}
The @FunctionalInterface annotation allows this interface to be implemented using lambda expressions or method references, providing flexibility in how strategies are instantiated.
Concrete Strategy Implementations
Individual algorithms are encapsulated in separate classes that implement the strategy interface. The repository provides three concrete implementations in MeleeStrategy.java, ProjectileStrategy.java, and SpellStrategy.java. Each class encapsulates a specific dragon-slaying algorithm:
- MeleeStrategy: Close combat approach
- ProjectileStrategy: Ranged attack approach
- SpellStrategy: Magical attack approach
These classes isolate algorithm-specific code, ensuring that changes to one strategy do not affect others.
The Context Class
The DragonSlayer class in DragonSlayer.java serves as the context that utilizes the strategy. It maintains a reference to the current strategy and provides mechanisms for both initial assignment and runtime switching:
public class DragonSlayer {
private DragonSlayingStrategy strategy;
public DragonSlayer(DragonSlayingStrategy strategy) {
this.strategy = strategy;
}
public void changeStrategy(DragonSlayingStrategy strategy) {
this.strategy = strategy;
}
public void goToBattle() {
strategy.execute();
}
}
The context class delegates the actual work to the strategy instance, remaining decoupled from specific algorithm implementations.
Runtime Algorithm Selection Mechanisms
The repository demonstrates four distinct approaches for selecting and switching algorithms at runtime, each suited to different architectural needs.
Constructor Injection
The most straightforward approach initializes the context with a specific strategy during object construction:
DragonSlayer slayer = new DragonSlayer(new MeleeStrategy());
slayer.goToBattle(); // Executes melee attack
This method establishes the initial algorithm but maintains the flexibility to change it later.
Dynamic Strategy Switching
The changeStrategy() method enables true runtime polymorphism by allowing algorithm substitution after object creation:
slayer.changeStrategy(new ProjectileStrategy());
slayer.goToBattle(); // Now executes projectile attack
This mechanism is crucial for applications requiring adaptive behavior based on user input, configuration changes, or evolving game states.
Lambda-Based Strategies
Because DragonSlayingStrategy is a functional interface, anonymous implementations can be provided using lambda expressions:
slayer.changeStrategy(() -> LOGGER.info("You cast a fireball spell!"));
slayer.goToBattle();
This approach eliminates the need to create separate classes for simple algorithms, reducing boilerplate code while maintaining the same architectural benefits.
Enum-Based Strategy Repository
The LambdaStrategy.java file demonstrates an elegant alternative using enums to store predefined strategy implementations:
public enum LambdaStrategy {
MeleeStrategy(() -> LOGGER.info("Melee attack!")),
ProjectileStrategy(() -> LOGGER.info("Projectile attack!")),
SpellStrategy(() -> LOGGER.info("Spell attack!"));
private final DragonSlayingStrategy strategy;
LambdaStrategy(DragonSlayingStrategy strategy) {
this.strategy = strategy;
}
public DragonSlayingStrategy get() {
return strategy;
}
}
Clients can select strategies using enum constants: slayer.changeStrategy(LambdaStrategy.MeleeStrategy.get()).
Practical Implementation Example
Below is a self-contained example adapting the repository's approach for a sorting utility. This demonstrates how to apply the pattern to runtime algorithm selection in a business context:
// Strategy interface
@FunctionalInterface
public interface SortingStrategy {
void sort(int[] data);
}
// Concrete strategies
public class BubbleSort implements SortingStrategy {
@Override
public void sort(int[] data) {
// Bubble sort implementation
for (int i = 0; i < data.length - 1; i++) {
for (int j = 0; j < data.length - i - 1; j++) {
if (data[j] > data[j + 1]) {
int temp = data[j];
data[j] = data[j + 1];
data[j + 1] = temp;
}
}
}
}
}
public class QuickSort implements SortingStrategy {
@Override
public void sort(int[] data) {
quickSort(data, 0, data.length - 1);
}
private void quickSort(int[] arr, int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
private int partition(int[] arr, int low, int high) {
int pivot = arr[high];
int i = (low - 1);
for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
i++;
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
return i + 1;
}
}
// Context class
public class Sorter {
private SortingStrategy strategy;
public Sorter(SortingStrategy strategy) {
this.strategy = strategy;
}
public void changeStrategy(SortingStrategy strategy) {
this.strategy = strategy;
}
public void sortArray(int[] data) {
strategy.sort(data);
}
}
// Runtime usage
public class Application {
public static void main(String[] args) {
int[] numbers = {64, 34, 25, 12, 22, 11, 90};
// Initial strategy selection
Sorter sorter = new Sorter(new BubbleSort());
sorter.sortArray(numbers);
// Runtime algorithm switch based on data size
if (numbers.length > 1000) {
sorter.changeStrategy(new QuickSort());
}
// Alternative: Lambda strategy for simple cases
sorter.changeStrategy(arr -> java.util.Arrays.sort(arr));
sorter.sortArray(numbers);
}
}
Design Benefits and SOLID Principles
The Strategy pattern implementation in the java-design-patterns repository exemplifies several key software design principles:
- Open/Closed Principle: New algorithms can be added by creating additional classes implementing
DragonSlayingStrategywithout modifying existing context code inDragonSlayer.java. - Single Responsibility: Each concrete strategy class encapsulates exactly one algorithm, making them easier to test, maintain, and debug independently.
- Loose Coupling: The context depends only on the abstraction (
DragonSlayingStrategy), not concrete implementations, enabling easy mocking during unit testing and allowing algorithms to vary without client knowledge.
Summary
- The Strategy pattern extracts algorithms into separate classes that share a common interface, enabling runtime selection via polymorphism.
- The
DragonSlayercontext class in the repository demonstrates algorithm injection through constructors and dynamic switching via thechangeStrategy()method. - Java 8 lambda expressions provide a concise alternative to concrete strategy classes when implementing the
DragonSlayingStrategyfunctional interface. - The enum-based approach in
LambdaStrategy.javaoffers type-safe, predefined algorithm collections suitable for configuration-driven selection. - This pattern eliminates conditional statements, promotes code reuse, and makes systems easier to extend with new algorithms.
Frequently Asked Questions
What is the difference between Strategy and Template Method patterns?
The Strategy pattern encapsulates entire algorithms in interchangeable objects, allowing complete algorithm replacement at runtime. In contrast, the Template Method pattern defines the skeleton of an algorithm in a base class, allowing subclasses to override specific steps while maintaining the overall structure. Use Strategy when you need to switch between different complete algorithms; use Template Method when algorithms share common steps but vary in specific details.
Can the Strategy pattern be implemented without interfaces?
While interfaces provide the cleanest implementation, abstract classes can serve as the strategy type when algorithms share common state or helper methods. However, using abstract classes reduces flexibility since Java only supports single inheritance. The repository's use of DragonSlayingStrategy as an interface maximizes flexibility and enables lambda implementations.
When should I use enum-based strategies versus concrete classes?
Use enum-based strategies in LambdaStrategy.java style when you have a fixed, finite set of algorithms that won't change at deployment time and are simple enough to express as lambdas. Use concrete classes like MeleeStrategy.java when algorithms require complex state management, external dependencies, or extensive configuration. Enums provide type safety and compilation-time checking, while classes offer greater flexibility for complex logic.
How does the Strategy pattern improve testability?
The pattern improves testability by allowing mock strategies to be injected into the context class during unit testing. Because DragonSlayer depends only on the DragonSlayingStrategy interface, test doubles can easily replace expensive or complex algorithms (like database-intensive sorting or external API calls) with lightweight stubs that verify the context interacts with the strategy correctly.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →