Ashutosh Krishna
Ashutosh Writes

Follow

Ashutosh Writes

Follow
A Beginner's Guide to Strategy Design Pattern

A Beginner's Guide to Strategy Design Pattern

A Step-by-Step Guide to Using the Strategy Design Pattern in Object-Oriented Programming for Flexible and Reusable Code

Ashutosh Krishna's photo
Ashutosh Krishna
ยทMay 1, 2023ยท

8 min read

Play this article

Table of contents

Introduction

The Strategy Design Pattern is a behavioral design pattern that allows you to dynamically change the behavior of an object by encapsulating it into different strategies. This pattern enables an object to choose from multiple algorithms and behaviors at runtime, rather than statically choosing a single one.

It is based on the principle of composition over inheritance. It defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime. The core idea behind this pattern is to separate the algorithms from the main object, allowing the object to delegate the algorithm's behavior to one of its contained strategies.

In simpler terms, the Strategy Design Pattern provides a way to extract the behavior of an object into separate classes that can be swapped in and out at runtime. This enables the object to be more flexible and reusable, as different strategies can be easily added or modified without changing the object's core code.

Benefits of using the Strategy Design Pattern

Using the Strategy Design Pattern can provide several benefits, including:

  1. Improved code flexibility: By encapsulating the behavior of an object into different strategies, the code becomes more flexible and easier to modify.

  2. Better code reusability: Since the strategies are encapsulated and interchangeable, they can be reused across different objects and projects.

  3. Encourages better coding practices: This pattern promotes good coding practices, such as separating concerns and reducing code complexity.

  4. Simplifies testing: By separating the algorithms and behaviors from the object, testing becomes more straightforward.

Use cases for the Strategy Design Pattern

The Strategy Design Pattern can be useful in various scenarios, such as:

  1. Sorting algorithms: Different sorting algorithms can be encapsulated into separate strategies and passed to an object that needs sorting.

  2. Validation rules: Different validation rules can be encapsulated into separate strategies and passed to an object that needs validation.

  3. Text formatting: Different formatting strategies can be encapsulated into separate strategies and passed to an object that needs formatting.

  4. Database access: Different database access strategies can be encapsulated into separate strategies and passed to an object that needs to access data from different sources.

  5. Payment strategy: Different payment methods can be encapsulated into separate strategies and passed to an object that needs to process payments.

Understanding the Strategy Design Pattern

The Strategy Design Pattern is a powerful pattern in the world of object-oriented programming. It provides a flexible way to encapsulate and swap the behavior of an object at runtime, enabling code to be more adaptable and easier to maintain. In this section, we will dive deeper into the Strategy Design Pattern, discussing its definition, components, and how it works.

Components of the Strategy Design Pattern

The Strategy Design Pattern consists of three primary components:

  1. Context: The object that will delegate its behavior to one of the contained strategies. The context maintains a reference to a strategy object and interacts with it through a common interface.

  2. Strategy Interface: The interface that defines the behavior for all strategies. The strategies implement this interface to provide their unique implementation of the behavior.

  3. Concrete Strategies: The classes that implement the Strategy Interface. Each strategy encapsulates a specific behavior that the context can switch to at runtime.

How the Strategy Design Pattern Works

The Strategy Design Pattern works by separating the behavior of an object from the object itself. The behavior is encapsulated into different strategies, each with its own implementation of the behavior. The context maintains a reference to a strategy object and interacts with it through a common interface. At runtime, the context can swap the current strategy with another one, effectively changing the object's behavior.

Examples of the Strategy Design Pattern in action

One example of the Strategy Design Pattern in action is in a music streaming service where different subscription tiers have different pricing models. Each subscription tier could have a different pricing strategy that encapsulates its unique pricing logic. The service's billing system would delegate the pricing calculation to the current subscription's strategy, allowing for easy modification and extension of the pricing logic.

Another example is the payment strategy. Different payment methods can be encapsulated into separate strategies, each with its own unique processing logic. A shopping cart application may use the Strategy Design Pattern to encapsulate credit card, PayPal, and cryptocurrency payment methods into separate strategies that can be swapped at runtime. The application's payment processing system would delegate the payment processing logic to the current payment method's strategy, allowing for easy modification and extension of the payment processing logic.

Implementing the Strategy Design Pattern

In this section, we will discuss how to implement the Strategy Design Pattern. We will start with a code example that violates the Strategy Design Pattern and explain the problems with it. Then, we will refactor the code to demonstrate how to implement the Strategy Design Pattern.

To implement the Strategy Design Pattern in Java, follow these steps:

  1. Identify the algorithm or behavior that needs to be encapsulated and made interchangeable.

  2. Define an interface that represents the behavior, with a single method signature that takes in any required parameters.

  3. Implement concrete classes that provide specific implementations of the behavior defined in the interface.

  4. Define a context class that holds a reference to the interface and calls its method when needed.

  5. Modify the context class to allow for the dynamic swapping of the concrete implementations at runtime.

Code Example

Let's consider the following code example:

package withoutstrategy;

public class PaymentProcessor {
    private PaymentType paymentType;

    public void processPayment(double amount) {
        if (paymentType == PaymentType.CREDIT_CARD) {
            System.out.println("Processing credit card payment of amount " + amount);
        } else if (paymentType == PaymentType.DEBIT_CARD) {
            System.out.println("Processing debit card payment of amount " + amount);
        } else if (paymentType == PaymentType.PAYPAL) {
            System.out.println("Processing PayPal payment of amount " + amount);
        } else {
            throw new IllegalArgumentException("Invalid payment type");
        }
    }

    public void setPaymentType(PaymentType paymentType) {
        this.paymentType = paymentType;
    }
}

enum PaymentType {
    CREDIT_CARD,
    DEBIT_CARD,
    PAYPAL
}

In this code, the PaymentProcessor class has a processPayment method that takes a payment amount and processes the payment. The payment type is set using the setPaymentType method, which sets the paymentType field. The processPayment method then checks the value of paymentType and processes the payment accordingly.

The problem with this code is that it violates the Open-Closed Principle, which states that classes should be open for extension but closed for modification. In this code, if you want to add a new payment type, you would have to modify the processPayment method, which violates the Open-Closed Principle.

The PaymentProcessor class violates the Strategy pattern by using conditional statements to determine the type of payment and then processing it accordingly. This approach can quickly become unmanageable and inflexible as the number of payment types increases.

To fix this problem, you can use the Strategy Design Pattern. First, you define a common interface for all payment strategies, which in this case is the PaymentStrategy interface:

package withstrategy;

public interface PaymentStrategy {
    void processPayment(double amount);
}

You then define concrete implementations of the PaymentStrategy interface for each payment type. For example, here are the CreditCardPaymentStrategy, DebitCardPaymentStrategy, and PaypalPaymentStrategy classes:

package withstrategy;

public class CreditCardPaymentStrategy implements PaymentStrategy {
    public void processPayment(double amount) {
        System.out.println("Processing credit card payment of amount " + amount);
    }
}
package withstrategy;

public class DebitCardPaymentStrategy implements PaymentStrategy {
    public void processPayment(double amount) {
        System.out.println("Processing debit card payment of amount " + amount);
    }
}
package withstrategy;

public class PaypalPaymentStrategy implements PaymentStrategy {
    public void processPayment(double amount) {
        System.out.println("Processing PayPal payment of amount " + amount);
    }
}

Finally, you update the PaymentProcessor class to take a PaymentStrategy object in its constructor, which it uses to process the payment:

package withstrategy;

public class PaymentProcessor {
    private PaymentStrategy paymentStrategy;

    public PaymentProcessor(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

    public void processPayment(double amount) {
        paymentStrategy.processPayment(amount);
    }
}

This implementation follows the Open-Closed Principle as well as Strategy Pattern because you can add new payment types by creating new implementations of the PaymentStrategy interface without modifying the existing code.

Best practices for implementing the Strategy Design Pattern

Here are a few best practices to keep in mind when implementing the Strategy Design Pattern:

  1. Keep the interface simple and focused on a single responsibility.

  2. Encapsulate any stateful behavior in the concrete strategy classes, rather than in the context class.

  3. Use dependency injection to pass the concrete strategy to the context class, rather than creating it directly in the context class.

  4. Use an enum or a factory class to provide a centralized place for creating and managing concrete strategy objects.

Real-world Applications of the Strategy Design Pattern

The Strategy Design Pattern has been used extensively in various real-world applications. One such example is the Java Collections Framework. The Collections Framework provides a set of interfaces and classes to represent collections of objects, such as lists, sets, and maps. The framework allows different strategies to be applied to collections based on their behavior.

For instance, the Collections Framework includes a sort() method that allows the sorting of collections. The sort() method takes a Comparator object as an argument, which is responsible for comparing objects within the collection. The Comparator interface defines a strategy for comparing two objects, and the sort() method uses this strategy to sort the collection.

In addition, the Collections Framework also includes the Iterator interface, which defines a strategy for accessing elements of a collection. The Iterator allows the user to traverse the collection without exposing its internal structure, which can change over time. By using the Iterator interface, the user can switch between different strategies for accessing elements of the collection.

Wrapping Up

In this tutorial, we have explored the Strategy Design Pattern and its implementation in Java. We have seen how the Strategy pattern can be used to separate the behavior of an object from its implementation, providing greater flexibility and maintainability in code.

We discussed the components of the Strategy Design Pattern, including the Context, Strategy Interface, and Concrete Strategies. We also provided an example of how the pattern can be used to implement a payment system, allowing for multiple payment options to be implemented using a single interface.

By separating the behavior of an object from its implementation, the Strategy pattern provides greater flexibility and adaptability to changing requirements.

Additional Resources

Did you find this article valuable?

Support Ashutosh Krishna by becoming a sponsor. Any amount is appreciated!

See recent sponsors |ย Learn more about Hashnode Sponsors
ย 
Share this