Simplify Strategy (Behavioural Pattern)
🌑 Why Strategy Method Pattern
As you may already know, Architect Spidey loves watching movies. Inspired by her love, Architect Spidey is working on an application that displays movie reviews.
In her application, movie reviews are already displayed. But now comes a new challenge: users want to sort the reviews based on their preferences. Some prefer the newest reviews first, others seek only the highest-rated ones, and some users want reviews based on location, perhaps from their hometown or region.
A simple solution is to create a ReviewSorter struct and pass an enum value from ReviewSortOption to retrieve sorted results. While this approach works for basic use cases, it has a drawback: every time a new sort type is introduced, the ReviewSorter must be modified to handle the new case. This breaks the Open/Closed Principle from the SOLID design principles, which states that software entities should be open for extension but closed for modification.
As a child, she enjoyed playing with LEGO, where each brick came in different shapes and sizes. Each brick served a unique purpose; she could choose and connect any of them to build whatever she imagined. Now, she wants to replicate that experience in code by creating different pieces of logic that can be selected at runtime, just like picking the right LEGO brick when needed.
🌥️ Outline
The Strategy Pattern is a behavioural design pattern that defines a family of interchangeable algorithms and encapsulates each separately. This allows the algorithm to be selected at runtime, enabling greater flexibility.
Strategy – Define a family of algorithms — swap them like 🧩 LEGO blocks, no code changes needed.
🌜 Strategy In Real World
When you’re buying groceries, the checkout process remains the same:
“Scan items → Choose payment method → Pay → Get receipt”.
Each payment method has its own way of processing, but the store doesn’t modify the entire checkout system every time. Instead, it selects the appropriate payment method (strategy) at runtime based on your choice.
🌕 Solution
Spidey defines a ReviewSort interface to sort the movie reviews. For each sorting type, she creates a separate struct that implements this interface
Then, she created a struct MovieReviewSorter that holds a ReviewSort and is responsible for applying the chosen sort to provide the desired result.
A question may come to mind: Why not let the client directly use a specific sorting struct instead of going through MovieReviewSorter?
While possible, this would scatter the logic of applying sorting strategies across multiple places, violating the Single Responsibility Principle.
The MovieReviewSorter acts as a centralised point that holds and applies the chosen strategy. This keeps the client code clean and makes it easier to extend or switch strategies in the future.
While this is a simple example, real-world scenarios often involve multiple parameters or dependencies. In such cases, using a dedicated context like MovieReviewSorter helps keep responsibilities separated and ensures scalability.
Strategy Components
Strategy Interface:
Defines a common method for all interchangeable behaviours and ensures that all strategies can be used interchangeably by the Context. ReviewSort is the Strategy interface used to define interchangeable sorting algorithms.
Concrete Strategies:
Implement the Strategy interface, each providing a specific behaviour. SortByDate, SortByRating, and SortByLocation are Concrete Strategies that implement the ReviewSort interface. Each of these strategies provides a specific sorting algorithm based on date, rating, or location.
Context:
It uses a strategy instance to delegate behaviour. It allows switching strategies dynamically without altering its code. MovieReviewSorter is the context struct that uses a strategy to sort movie reviews.
Did you know?
In games like Age of Empires, AI uses different strategies (aggressive, defensive, or passive) depending on the game conditions. This is achieved using the Strategy Pattern, where different strategies for behaviour are dynamically swapped at runtime.
🌟 Where To Apply
Use the Strategy When You Have Interchangeable Algorithms
The Strategy pattern allows you to alter an object’s behaviour indirectly at runtime by associating it with interchangeable sub-objects — each representing a specific way to perform a task. This is especially useful when you have a family of behaviours and want to switch between them dynamically without modifying the original code. Like sorting, payment method, etc.
Use the Strategy To Transform Conditional Statements
When you have a complex set of if-else or switch statements to choose different behaviours, the Strategy pattern allows you to encapsulate each behaviour into its own class. This way, instead of writing many conditions, you can switch between the strategies at runtime, making the code more flexible and easier to extend.
Example: In a canvas-based drawing app, users can choose different tools (e.g., Brush, Line, Rectangle) to interact with the canvas. Instead of using conditional statements (like if or switch) to check which tool the user selects, each tool can be treated as a strategy, allowing the user to switch between them at runtime, without changing the underlying code.
Use Strategy To Separate Core Logic From changing behavior
The Strategy pattern separates core logic from variable behaviours, making the main code cleaner and more focused. The Strategy pattern breaks functionality into variations, avoiding mixing the core logic with complex conditionals by encapsulating behaviours in separate classes that can be swapped at runtime based on context.
Example: Suppose an application supports push notifications, and upon receiving a notification, the core logic updates the internal state. The specific action (sound, pop-up, etc.) to be taken depends on the notification type. Instead of cluttering the core logic with conditionals, the Strategy pattern allows different behaviours to be handled by separate strategies, keeping the core logic clean and focused.
🌓 To Gain Something, You Must Trade Off
Design patterns are solutions to common problems in software design. They provide a structured approach to solving issues, much like a blueprint. While it’s possible to solve a problem without using a design pattern, applying one offers better flexibility, scalability, and maintainability. However, each benefit comes with its trade-off. Let’s examine this concept in the context of the Strategy Pattern.
Flexibility vs Complexity
The Strategy pattern enables easy switching between different behaviours (algorithms or operations) at runtime, enhancing the adaptability of your system to changing requirements.
However, it also increases the number of classes, which can add complexity to the codebase. Additionally, managing the initialisation of strategies may introduce extra complexity, requiring careful assignment of the right strategy at the right time, potentially leading to the use of more advanced object construction patterns like factories.
Maintainability vs Debugging
The Strategy pattern makes it easier to update and maintain your code by separating different behaviours into their classes. This modular approach allows you to modify or add new behaviours without affecting the rest of the code, making it simpler to manage and extend in the future. As Spidey, using ReviewSort can introduce a new sorting method without impacting the core logic.
However, it may introduce slight overhead due to the dynamic selection of strategies at runtime. This can make it harder to trace the flow of control, and debugging may become more challenging, especially when issues arise from the interaction between strategies.
Decoupling vs Over-Engineering
The Strategy pattern enhances code modularity by separating core logic from variable behaviours, leading to cleaner and more maintainable code.
However, if behaviours rarely change or there are only a few variations, applying the pattern might introduce unnecessary complexity. In such cases, it can feel like over-engineering rather than a practical design improvement.
🌚 Some Interesting Facts
Strategy & Command Pattern
The Strategy and Command patterns may look similar at first glance—they both encapsulate behaviour into separate classes—but they serve different purposes and shine in different situations.
The Strategy pattern focuses on how a task is done by encapsulating different behaviours or algorithms into separate classes. It allows the system to choose the appropriate behaviour at runtime, making it easy to switch logic dynamically.
On the other hand, the Command pattern encapsulates what task should be done. Each action is wrapped in its class, allowing you to flexibly queue, log, undo, or execute commands.
Strategy is about choosing how to do something & Command is about deciding what to do and when.
Strategy & Template Method Pattern
The Strategy Pattern is used when a process has multiple independent variants. Each variant is encapsulated in a separate class, allowing the behaviour to be selected at runtime. This pattern promotes flexibility and clean separation of concerns. A common example is sorting algorithms in many programming languages.
The Template Method Pattern is used when a process has a fixed structure, but some steps may vary. The base class defines the overall flow, while subclasses override specific parts of the process. This ensures consistency while allowing controlled customisation..
Open/Closed Principle & Strategy Pattern
The Open/Closed Principle states that a class should be open for extension but closed for modification—allowing new features to be added without changing existing code.
The Strategy pattern is one of the strongest supporters of this principle. It allows you to introduce new behaviours (strategies) by simply creating new classes, without altering the existing logic. This keeps the core codebase stable and adaptable, supporting future growth with minimal risk..
Code Example (Swift)
Strategy Interface:
1
2
3
4
5
6
7
8
9
10
11
struct MovieReview {
var username: String
var userCity: String
var rating: Double
var review: String
var date: String
}
protocol ReviewSort {
func sortReviews(_ reviews: [MovieReview]) -> [MovieReview]
}
Concrete Strategy:
1
2
3
4
5
struct SortByRating: ReviewSort {
func sortReviews(_ reviews: [MovieReview]) -> [MovieReview] {
//Highest Rated First
}
}
1
2
3
4
5
struct SortByDate: ReviewSort {
func sortReviews(_ reviews: [MovieReview]) -> [MovieReview] {
//Latest Date First sort
}
}
1
2
3
4
5
struct SortByLocation: ReviewSort {
func sortReviews(_ reviews: [MovieReview]) -> [MovieReview] {
//City Distance sort
}
}
Context:
1
2
3
4
5
6
7
8
9
10
11
struct ReviewSorter {
private var strategy: ReviewSort
init(strategy: ReviewSort) {
self.strategy = strategy
}
func sortReviews(_ reviews: [MovieReview]) -> [MovieReview] {
return strategy.sortReviews(reviews)
}
}
Client:
1
2
3
4
5
6
7
8
let spiderMovieReviews = [
MovieReview(username: "Spidey", userCity: "Delhi", rating: 5.0, review: "Very Good Movie", date: "13-01-25"),
MovieReview(username: "Orangu", userCity: "Mumbai", rating: 4.0, review: "Good Movie", date: "16-01-25"),
MovieReview(username: "Weaver", userCity: "Bangalore", rating: 5.0, review: "Good Movie", date: "13-01-25")
]
var sorter = ReviewSorter(strategy: SortByRating())
let sortedByRating = sorter.sortReviews(spiderMovieReviews)
Final Thoughts
This is one of Spidey’s favourite patterns—thanks to its plug-and-play flexibility and its elegant separation of concerns. But beware! His fondness for it has occasionally led to bouts of over-engineering. If you ever spot him going overboard, don’t hesitate to write to Spidey. He’s always open to a friendly nudge (and a code review)!
Comments powered by Disqus.