When designing complex software systems, it is crucial to maintain flexibility and reduce tightly coupled code. Behavioral design patterns help us achieve this by defining how classes and objects interact with one another. Among these patterns, the Strategy Pattern is a powerful tool that enables an algorithm's behavior to be selected at runtime, promoting flexibility and reusability.
What is the Strategy Pattern?
The Strategy Pattern is a behavioral design pattern that allows you to define a family of algorithms, encapsulate each one, and make them interchangeable. The key aspect of this pattern is that it enables the client to choose an algorithm from a set of algorithms dynamically, ensuring that the class is not dependent on specific implementations of an algorithm.
The core idea is that the strategy (algorithm) can vary independently from the clients that use it, making it a perfect fit for scenarios where the business logic depends on a changing algorithm.
Real-World Example
Let's take a simple example of filtering a list of integers. Based on different strategies, we can filter the integers in different ways. We will apply the Strategy Pattern to handle these filters, where each filter is a separate strategy.
Step-by-Step Breakdown
- Define the Strategy Interface
We need an interface that will define the filter method. Each concrete strategy will implement this interface.
package strategyPattern;
import java.util.List;
public interface FilterStrategy {
List<Integer> filter(List<Integer> intList);
}
- Implement Different Filter Strategies
Now, we will create several classes, each implementing the FilterStrategy
interface with a different filtering criterion.
package strategyPattern;
import java.util.List;
import java.util.stream.Collectors;
// Strategy to filter numbers greater than 20
public class GreaterThanTwentyFilter implements FilterStrategy {
@Override
public List<Integer> filter(List<Integer> intList) {
return intList.stream().filter(x -> x > 20).collect(Collectors.toList());
}
}
// Strategy to filter numbers divisible by 200
public class DivisibleBy200Filter implements FilterStrategy {
@Override
public List<Integer> filter(List<Integer> intList) {
return intList.stream().filter(x -> x % 200 == 0).collect(Collectors.toList());
}
}
// Strategy to filter numbers less than 5000
public class LessThan5000Filter implements FilterStrategy {
@Override
public List<Integer> filter(List<Integer> intList) {
return intList.stream().filter(x -> x < 5000).collect(Collectors.toList());
}
}
- Context Class
The context class is responsible for maintaining a reference to the chosen strategy and delegating the operation to the selected algorithm.
package strategyPattern;
import java.util.List;
public class NumberFilterContext {
private FilterStrategy strategy;
// Setting the strategy at runtime
public void setStrategy(FilterStrategy strategy) {
this.strategy = strategy;
}
public List<Integer> executeStrategy(List<Integer> intList) {
return strategy.filter(intList);
}
}
- Client Code (Main Method)
Finally, the client code chooses the strategy dynamically based on requirements and applies it.
package strategyPattern;
import java.util.List;
public class StrategyPatternDemo {
public static void main(String[] args) {
List<Integer> intList = List.of(100, 2, 300, 67, 56, 34, 12, 34, 33, 21, 66, 78, 79, 98, 56, 78, 65);
// Create context
NumberFilterContext context = new NumberFilterContext();
// Strategy 1: Numbers greater than 20
context.setStrategy(new GreaterThanTwentyFilter());
List<Integer> result1 = context.executeStrategy(intList);
System.out.println("Filtered with GreaterThanTwentyFilter: " + result1);
// Strategy 2: Numbers divisible by 200
context.setStrategy(new DivisibleBy200Filter());
List<Integer> result2 = context.executeStrategy(intList);
System.out.println("Filtered with DivisibleBy200Filter: " + result2);
// Strategy 3: Numbers less than 5000
context.setStrategy(new LessThan5000Filter());
List<Integer> result3 = context.executeStrategy(intList);
System.out.println("Filtered with LessThan5000Filter: " + result3);
}
}
Advantages of the Strategy Pattern
Flexibility: You can change the strategy without modifying the client code, making it highly flexible.
Reusability: Different strategies are encapsulated into separate classes, improving code reusability.
Open-Closed Principle: The Strategy Pattern adheres to the Open-Closed Principle by allowing new strategies to be added without changing existing code.
Use Cases
The Strategy Pattern is highly useful in scenarios where:
You have multiple ways of performing the same action, such as sorting or filtering, and want to switch between them dynamically.
You want to avoid multiple
if-else
orswitch-case
conditions that decide which algorithm to use.
Conclusion
The Strategy Pattern is an excellent way to allow dynamic selection of algorithms at runtime, offering clean separation between the client and the specific implementations. This ensures that your code remains flexible, maintainable, and adheres to fundamental design principles.
By encapsulating each filtering strategy separately, as demonstrated in our example, you can now easily add or modify filters without changing the core logic. This leads to clean, testable, and scalable code.