Strategy Design Pattern: An Easy Learner

Strategy Design Pattern: An Easy Learner

19 Sep 2024
Intermediate
56.9K Views
32 min read
Learn with an interactive course and practical hands-on labs

⭐ .NET Design Patterns Course: Design Patterns in C# Online Training

Strategy Design Pattern

Strategy Design Pattern lets you choose and change an algorithm at runtime without hardcoding it. It puts different algorithms in separate classes so they can be easily swapped when needed. This is helpful in situations like sorting data or handling payments, where different approaches might be needed.

In the Design Pattern tutorial, we will discuss about what is the Strategy design pattern?, the components of strategy design pattern, communication between components, real-world analogy, strategy design pattern example, applications of strategy design pattern, and many more.

What is the Strategy Design Pattern?

The strategy design pattern is a type of behavioral design pattern that enables runtime selection of an object's behavior. One of the popular design patterns in object-oriented programming belongs to the Gang of Four (GoF) design pattern. It enables you to design a family of algorithms, package them all together, and provide interoperability between them.

  • By using this pattern, you may group together different algorithms or behaviors into distinct classes or strategies.
  • Each strategy provides a clear and modular mechanism to handle various variations or implementations by encapsulating a particular behavior or algorithm.
  • This pattern allows clients to modify their behavior dynamically and flexibly during runtime, allowing them to transition between multiple techniques.
  • The pattern promotes cooperation between context and strategy objects by delegating the execution of a behavior to a strategy object from the context.
Read More:
Different Types of Design Patterns
Different Types of Software Design Principles

Components of Strategy Design Pattern

There are four major components of the Strategy design pattern that are:

  1. Context
  2. Strategy Interface
  3. Concrete Strategy
  4. Client

Components of Strategy Design Pattern

1. Context

  • Context is the main class that uses a strategy to perform tasks.
  • It doesn't do the actual work but asks a strategy (algorithm) to do it.
  • It refers to a strategy that can be changed at any time.
  • Allows flexibility by switching strategies without modifying its own code.

2. Strategy Interface

  • Strategy Interface defines a common method or set of methods that all strategies must follow.
  • It acts as a blueprint for different algorithms or behaviors.
  • Each concrete strategy class implements this interface to provide its own version of the algorithm.
  • The context class interacts with the strategy via this interface, ensuring the flexibility and interchangeability of algorithms.
  • The interface ensures that the context doesn't need to know the details of the specific algorithm being used.

3. Concrete Strategy

  • Concrete Strategy is a class that implements the Strategy interface.
  • It provides a specific implementation of the algorithm or behavior defined by the Strategy interface.
  • Multiple concrete strategies can exist, each offering a different version of the algorithm.
  • These classes are interchangeable, allowing the context to switch between them at runtime.
  • Concrete strategies are responsible for the actual work or logic based on the chosen algorithm.

4. Client

  • Theclientisthe part of the system that interacts with the Context and triggers the use of a strategy.
  • It chooses which specific concrete strategy the Context should use, often bypassing the selected strategy to the Context.
  • The client doesn't need to know how the strategy works; they only need to know which strategy to use.
  • By using the Strategy Design Pattern, the client can easily switch between different algorithms without changing its own code.
  • The client initiates the process by providing the necessary input to the Context, which then delegates the task to the chosen strategy.

Communication between the Components

Communication between the Components

1. Client to Context

  • The Client engages with the Context to start the task execution since it is aware of the requirements for the task.
  • Based on the demands of the work, the Client chooses a suitable approach and gives it to the Context.
  • If required, the Client may set up the chosen approach before sending it to the Context.

2. Context to Strategy

  • The Context assigns the job to the chosen approach and maintains a reference to it.
  • The particular algorithm or action included within the strategy is executed when the Context calls a method on the strategy object.

3. Strategy to Context

  • Once the strategy completes its execution, it may return a result or perform any necessary actions.
  • The strategy communicates the result or any relevant information back to the Context, which may further process or utilize the result as needed.

4. Strategy Interface as Contract

  • The Strategy Interface serves as a contract that defines a set of methods that all concrete strategies must implement.
  • The Context communicates with strategies through the common interface, promoting interchangeability and decoupling.

5. Decoupled Communication

  • Communication between the components is decoupled, meaning that the Context does not need to know the specific details of how each strategy implements the task.
  • Strategies can be swapped or replaced without impacting the client or other strategies as long as they adhere to the common interface.

Implementation of Strategy Design Pattern

  1. Strategy Interface: Defines an interface common to all supported algorithms.
  2. Concrete Strategy Classes: Implements different algorithms that implement the Strategy interface.
  3. Context Class: Uses the Strategy interface to call the chosen algorithm.
  4. Use Strategy Pattern: Then use the strategy pattern in your code

Real-World Example of Strategy Design Pattern

Problem: In an e-commerce platform, you may have different shipping options, each calculated differently. Using the Strategy Design Pattern allows the system to easily switch between these algorithms at runtime based on user choice without modifying the core logic of the application.

An e-commerce platform offers different shipping methods:

  • Standard Shipping
  • Express Shipping
  • Same Day Delivery

The platform needs to select a shipping strategy based on the customer’s choice, but the method for calculating the shipping cost should be flexible and changeable at runtime.

Let's explore this concept in different languages, such as C# Compiler, Java Compiler, Python Compiler, JavaScript Compiler, and TypeScript Compiler.

using System;

// Strategy interface
public interface ShippingStrategy
{
    double calculateShippingCost(double packageWeight);
}

// Concrete Strategy 1: Standard Shipping
public class StandardShipping : ShippingStrategy
{
    public double calculateShippingCost(double packageWeight)
    {
        return packageWeight * 10;  // Example calculation
    }
}

// Concrete Strategy 2: Express Shipping
public class ExpressShipping : ShippingStrategy
{
    public double calculateShippingCost(double packageWeight)
    {
        return packageWeight * 20;  // Example calculation
    }
}

// Concrete Strategy 3: Same Day Delivery
public class SameDayDelivery : ShippingStrategy
{
    public double calculateShippingCost(double packageWeight)
    {
        return packageWeight * 50;  // Example calculation
    }
}

// Context class
public class Order
{
    private ShippingStrategy shippingStrategy;

    // Setting a shipping strategy
    public void setShippingStrategy(ShippingStrategy shippingStrategy)
    {
        this.shippingStrategy = shippingStrategy;
    }

    // Calculate shipping cost based on strategy
    public double calculateShipping(double packageWeight)
    {
        return shippingStrategy.calculateShippingCost(packageWeight);
    }
}

// Client code
class Program
{
    static void Main(string[] args)
    {
        Order order = new Order();

        double packageWeight = 5.0; // Example weight

        // Using Standard Shipping
        order.setShippingStrategy(new StandardShipping());
        Console.WriteLine("Standard Shipping Cost: " + order.calculateShipping(packageWeight));

        // Switching to Express Shipping
        order.setShippingStrategy(new ExpressShipping());
        Console.WriteLine("Express Shipping Cost: " + order.calculateShipping(packageWeight));

        // Switching to Same Day Delivery
        order.setShippingStrategy(new SameDayDelivery());
        Console.WriteLine("Same Day Delivery Cost: " + order.calculateShipping(packageWeight));
    }
}         
from abc import ABC, abstractmethod

# Strategy interface
class ShippingStrategy(ABC):
    @abstractmethod
    def calculateShippingCost(self, packageWeight):
        pass

# Concrete Strategy 1: Standard Shipping
class StandardShipping(ShippingStrategy):
    def calculateShippingCost(self, packageWeight):
        return packageWeight * 10  # Example calculation

# Concrete Strategy 2: Express Shipping
class ExpressShipping(ShippingStrategy):
    def calculateShippingCost(self, packageWeight):
        return packageWeight * 20  # Example calculation

# Concrete Strategy 3: Same Day Delivery
class SameDayDelivery(ShippingStrategy):
    def calculateShippingCost(self, packageWeight):
        return packageWeight * 50  # Example calculation

# Context class
class Order:
    def __init__(self):
        self.shippingStrategy = None
    
    # Setting a shipping strategy
    def setShippingStrategy(self, shippingStrategy):
        self.shippingStrategy = shippingStrategy
    
    # Calculate shipping cost based on strategy
    def calculateShipping(self, packageWeight):
        return self.shippingStrategy.calculateShippingCost(packageWeight)

# Client code
if __name__ == "__main__":
    order = Order()
    
    packageWeight = 5.0  # Example weight

    # Using Standard Shipping
    order.setShippingStrategy(StandardShipping())
    print("Standard Shipping Cost:", order.calculateShipping(packageWeight))

    # Switching to Express Shipping
    order.setShippingStrategy(ExpressShipping())
    print("Express Shipping Cost:", order.calculateShipping(packageWeight))

    # Switching to Same Day Delivery
    order.setShippingStrategy(SameDayDelivery())
    print("Same Day Delivery Cost:", order.calculateShipping(packageWeight))            
// Strategy interface
interface ShippingStrategy {
    double calculateShippingCost(double packageWeight);
}

// Concrete Strategy 1: Standard Shipping
class StandardShipping implements ShippingStrategy {
    @Override
    public double calculateShippingCost(double packageWeight) {
        return packageWeight * 10;  // Example calculation
    }
}

// Concrete Strategy 2: Express Shipping
class ExpressShipping implements ShippingStrategy {
    @Override
    public double calculateShippingCost(double packageWeight) {
        return packageWeight * 20;  // Example calculation
    }
}

// Concrete Strategy 3: Same Day Delivery
class SameDayDelivery implements ShippingStrategy {
    @Override
    public double calculateShippingCost(double packageWeight) {
        return packageWeight * 50;  // Example calculation
    }
}

// Context class
class Order {
    private ShippingStrategy shippingStrategy;
    
    // Setting a shipping strategy
    public void setShippingStrategy(ShippingStrategy shippingStrategy) {
        this.shippingStrategy = shippingStrategy;
    }
    
    // Calculate shipping cost based on strategy
    public double calculateShipping(double packageWeight) {
        return shippingStrategy.calculateShippingCost(packageWeight);
    }
}

// Client code
public class Main {
    public static void main(String[] args) {
        Order order = new Order();
        
        double packageWeight = 5.0; // Example weight

        // Using Standard Shipping
        order.setShippingStrategy(new StandardShipping());
        System.out.println("Standard Shipping Cost: " + order.calculateShipping(packageWeight));

        // Switching to Express Shipping
        order.setShippingStrategy(new ExpressShipping());
        System.out.println("Express Shipping Cost: " + order.calculateShipping(packageWeight));

        // Switching to Same Day Delivery
        order.setShippingStrategy(new SameDayDelivery());
        System.out.println("Same Day Delivery Cost: " + order.calculateShipping(packageWeight));
    }
}            
// Strategy interface (implemented as a class in JavaScript)
class ShippingStrategy {
    calculateShippingCost(packageWeight) {
        throw new Error("This method should be overridden!");
    }
}

// Concrete Strategy 1: Standard Shipping
class StandardShipping extends ShippingStrategy {
    calculateShippingCost(packageWeight) {
        return packageWeight * 10;  // Example calculation
    }
}

// Concrete Strategy 2: Express Shipping
class ExpressShipping extends ShippingStrategy {
    calculateShippingCost(packageWeight) {
        return packageWeight * 20;  // Example calculation
    }
}

// Concrete Strategy 3: Same Day Delivery
class SameDayDelivery extends ShippingStrategy {
    calculateShippingCost(packageWeight) {
        return packageWeight * 50;  // Example calculation
    }
}

// Context class
class Order {
    constructor() {
        this.shippingStrategy = null;
    }

    // Setting a shipping strategy
    setShippingStrategy(shippingStrategy) {
        this.shippingStrategy = shippingStrategy;
    }

    // Calculate shipping cost based on strategy
    calculateShipping(packageWeight) {
        if (!this.shippingStrategy) {
            throw new Error("Shipping strategy not set.");
        }
        return this.shippingStrategy.calculateShippingCost(packageWeight);
    }
}

// Client code
const order = new Order();
const packageWeight = 5.0; // Example weight

// Using Standard Shipping
order.setShippingStrategy(new StandardShipping());
console.log("Standard Shipping Cost:", order.calculateShipping(packageWeight));

// Switching to Express Shipping
order.setShippingStrategy(new ExpressShipping());
console.log("Express Shipping Cost:", order.calculateShipping(packageWeight));

// Switching to Same Day Delivery
order.setShippingStrategy(new SameDayDelivery());
console.log("Same Day Delivery Cost:", order.calculateShipping(packageWeight));            
// Strategy interface
interface ShippingStrategy {
    calculateShippingCost(packageWeight: number): number;
}

// Concrete Strategy 1: Standard Shipping
class StandardShipping implements ShippingStrategy {
    calculateShippingCost(packageWeight: number): number {
        return packageWeight * 10;  // Example calculation
    }
}

// Concrete Strategy 2: Express Shipping
class ExpressShipping implements ShippingStrategy {
    calculateShippingCost(packageWeight: number): number {
        return packageWeight * 20;  // Example calculation
    }
}

// Concrete Strategy 3: Same Day Delivery
class SameDayDelivery implements ShippingStrategy {
    calculateShippingCost(packageWeight: number): number {
        return packageWeight * 50;  // Example calculation
    }
}

// Context class
class Order {
    private shippingStrategy: ShippingStrategy | null = null;

    // Setting a shipping strategy
    setShippingStrategy(shippingStrategy: ShippingStrategy): void {
        this.shippingStrategy = shippingStrategy;
    }

    // Calculate shipping cost based on strategy
    calculateShipping(packageWeight: number): number {
        if (!this.shippingStrategy) {
            throw new Error("Shipping strategy not set.");
        }
        return this.shippingStrategy.calculateShippingCost(packageWeight);
    }
}

// Client code
const main = () => {
    const order = new Order();
    
    const packageWeight = 5.0; // Example weight

    // Using Standard Shipping
    order.setShippingStrategy(new StandardShipping());
    console.log("Standard Shipping Cost: " + order.calculateShipping(packageWeight));

    // Switching to Express Shipping
    order.setShippingStrategy(new ExpressShipping());
    console.log("Express Shipping Cost: " + order.calculateShipping(packageWeight));

    // Switching to Same Day Delivery
    order.setShippingStrategy(new SameDayDelivery());
    console.log("Same Day Delivery Cost: " + order.calculateShipping(packageWeight));
};

main();   

Output

Standard Shipping Cost: 50
Express Shipping Cost: 100
Same Day Delivery Cost: 250     

Explanation

In this example,

  • ShippingStrategy: Interface that declares the method for calculating shipping costs.
  • Concrete Strategies (StandardShipping, ExpressShipping, SameDayDelivery): Implement different algorithms for calculating shipping costs.
  • Order: Acts as the context where you can set the strategy dynamically and calculate the shipping cost based on the selected strategy.

Applications of Strategy Design Pattern

Here are the several applications of Strategy Design patterns that are:

  • Sorting Algorithms: Dynamically choose different sorting methods (e.g., Bubble Sort, Quick Sort) based on the dataset or user needs.
  • Compression Algorithms: Switch between different file compression methods (e.g., ZIP, GZIP) in file management applications.
  • Payment Methods: Implement multiple payment options (e.g., credit card, PayPal, cryptocurrency) in e-commerce platforms.
  • Data Validation: Apply various validation techniques for input data (e.g., email, phone numbers) in web forms or data processing.
  • Discount Calculation: Offer different discount strategies (e.g., percentage-based, flat rate) in retail or sales systems.
  • Game AI Behavior: Define different AI strategies (e.g., aggressive, defensive) for game characters.

When to use Strategy Design Pattern

  • When multiple algorithms or behaviors are required: Use when you need to switch between different algorithms or behaviors dynamically.
  • When algorithms can vary based on context: Use when a class should be able to choose from different behaviors or algorithms depending on the situation.
  • To avoid conditional logic (if-else/switch): Use to eliminate complex conditional logic and make the code more maintainable.
  • When you want to isolate and encapsulate algorithms: Use to separate algorithms from the core logic, making them easier to modify or extend.
  • When behavior changes often: Use if you anticipate frequent changes to the logic or algorithms in the future.
  • Reusability is important: Use it when you want to reuse different strategies independently across various parts of the system.

When not to use Strategy Design Pattern

  • When behavior rarely changes: If the algorithms or behaviors are unlikely to change, there's no need for the extra complexity of this pattern.
  • When you have a simple system: If the system is small and straightforward, the Strategy Pattern may introduce unnecessary overhead.
  • When performance is critical: Switching strategies at runtime can introduce slight overhead. If performance is a concern, avoid unnecessary abstraction.
  • When there are too few strategies: If there is only one algorithm or a very limited number of strategies, using this pattern may complicate the design without adding value.
  • When client code complexity increases: If the clients are forced to manage strategies explicitly, the system might become more difficult to understand and maintain.

Connections with Other Design Patterns

  • Factory Pattern: The factory can instantiate different strategies dynamically, encapsulating the strategy creation process.
  • Decorator Pattern: The decorator adds extra functionality to objects, while Strategy changes their behavior. They can work together to add behavior and features.
  • Template Method PatternTemplate Method defines a fixed structure, while Strategy allows swapping entire algorithms. Both aim to manage algorithm logic differently.
  • State PatternBoth patterns change behavior at runtime, but the State manages transitions between states, while Strategy focuses on selecting different algorithms.
  • Command PatternCommand encapsulates actions as objects; Strategy encapsulates algorithms. They can work together when commands need flexible behavior.

Advantages of Strategy Design Pattern

  • Promotes flexibility: Allows changing algorithms or behaviors at runtime without modifying the client code.
  • Eliminates complex conditional logic: Replaces if-else or switch statements with interchangeable strategy classes.
  • Encourages code reuse: Strategies are encapsulated in separate classes, making them reusable across different contexts.
  • Supports open/closed principle: New strategies can be added without altering existing code.
  • Improves maintainability: By separating algorithms into distinct classes, code becomes easier to maintain and extend.

Disadvantages of Strategy Design Pattern

  • Increased number of classes: Each strategy is implemented in its own class, which can lead to a more complex and cluttered design.
  • Client must be aware of all strategies: The client needs to understand and choose the appropriate strategy, adding to complexity.
  • No strategy reuse if behaviors are too simple: If the behaviors are very simple, separating them into different strategies might add unnecessary overhead without real benefits.
  • Potential performance overhead: Switching strategies dynamically at runtime can introduce a small performance cost, especially if done frequently.
  • Tight coupling to Context class: If the strategies require many details about the context, it can lead to increased coupling between the strategy and context classes.
Read More:
Top 50 Java Design Patterns Interview Questions and Answers
.Net Design Patterns Interview Questions, You Must Know!
Most Frequently Asked Software Architect Interview Questions and Answers
Conclusion

In conclusion, we have seen that the Strategy Design Pattern enhances flexibility and maintainability by allowing dynamic algorithm selection and promoting code reuse. However, it can lead to increased class complexity and potential performance overhead. Also, consider Scholarhat's Software Architecture and Design Certification Training for a better understanding of other Java concepts.

FAQs

Yes, in functional programming, strategies can be implemented as first-class functions or closures, which can be passed around and invoked as needed. 

Yes, it can be combined with other design patterns like the Factory Method Pattern to create instances of strategies, or the Observer Pattern to notify changes in the strategy.

The Context class holds a reference to a Strategy object and delegates the algorithm execution to the strategy. It provides a way to configure the strategy and can switch strategies dynamically. 

Strategy initialization and configuration can be managed within the Context class or through a factory method. In some cases, dependency injection frameworks can handle the creation and configuration of strategies.

Yes, the Strategy Pattern can be implemented in various object-oriented languages such as C++, Python, C#, and others. The core principles of defining strategies, encapsulating algorithms, and using a context class are applicable across different languages. 
Share Article
About Author
Shailendra Chauhan (Microsoft MVP, Founder & CEO at Scholarhat by DotNetTricks)

Shailendra Chauhan is the Founder and CEO at ScholarHat by DotNetTricks which is a brand when it comes to e-Learning. He provides training and consultation over an array of technologies like Cloud, .NET, Angular, React, Node, Microservices, Containers and Mobile Apps development. He has been awarded Microsoft MVP 9th time in a row (2016-2024). He has changed many lives with his writings and unique training programs. He has a number of most sought-after books to his name which has helped job aspirants in cracking tough interviews with ease.
Accept cookies & close this