How to Implement a Strategy Design Pattern in Java

Avatar

By squashlabs, Last Updated: August 3, 2023

How to Implement a Strategy Design Pattern in Java

Introduction to the Strategy Design Pattern

The Strategy Design Pattern is a behavioral design pattern that allows you to define a family of algorithms, encapsulate each one as a separate class, and make them interchangeable at runtime. This pattern enables the algorithm to vary independently from clients that use it.

To implement the Strategy Design Pattern, you typically create an interface or abstract class that represents the strategy. Concrete classes are then created to implement different variations of the strategy. The client code can then select and use a particular strategy object without having to know the details of its implementation.

The Strategy Design Pattern promotes loose coupling between the client and the strategies, allowing for flexibility and easier maintenance of the codebase.

Example:

public interface SortingStrategy {
    void sort(int[] numbers);
}

public class BubbleSort implements SortingStrategy {
    public void sort(int[] numbers) {
        // Bubble sort implementation
    }
}

public class QuickSort implements SortingStrategy {
    public void sort(int[] numbers) {
        // Quick sort implementation
    }
}

public class SortingContext {
    private SortingStrategy strategy;

    public SortingContext(SortingStrategy strategy) {
        this.strategy = strategy;
    }

    public void setStrategy(SortingStrategy strategy) {
        this.strategy = strategy;
    }

    public void sortNumbers(int[] numbers) {
        strategy.sort(numbers);
    }
}

public class Client {
    public static void main(String[] args) {
        int[] numbers = {5, 1, 3, 2, 4};

        SortingContext context = new SortingContext(new BubbleSort());
        context.sortNumbers(numbers);

        context.setStrategy(new QuickSort());
        context.sortNumbers(numbers);
    }
}

In this example, the SortingStrategy interface defines the contract for different sorting algorithms. The BubbleSort and QuickSort classes implement this interface and provide their own sorting logic. The SortingContext class acts as a client and maintains a reference to the current strategy. The Client class demonstrates how different strategies can be applied to sort an array of numbers.

Related Article: How to Use the Xmx Option in Java

Pros and Cons of the Strategy Design Pattern

The Strategy Design Pattern offers several advantages:

– Flexibility: Strategies can be easily added, removed, or modified without affecting the client code.
– Reusability: Strategies can be reused in different contexts or by different clients.
– Testability: Each strategy can be tested independently, which promotes easier unit testing.
– Encapsulation: Each strategy encapsulates a specific algorithm, making the code more modular and maintainable.

However, there are also some considerations to keep in mind:

– Increased complexity: The Strategy Design Pattern introduces additional classes and indirection, which can make the codebase more complex.
– Runtime selection: The client must select the appropriate strategy at runtime, which may introduce additional decision-making logic.

Comparison of the Strategy Design Pattern with Other Java Patterns

The Strategy Design Pattern is often compared to other design patterns that serve similar purposes. Let’s take a look at a few of them:

State Design Pattern:

The State Design Pattern is similar to the Strategy Design Pattern as both involve encapsulating different behavior into separate classes. However, the State pattern focuses on managing the internal state of an object and transitioning between states, while the Strategy pattern focuses on interchangeable algorithms.

Related Article: Can Two Java Threads Access the Same MySQL Session?

Template Method Design Pattern:

The Template Method Design Pattern defines the skeleton of an algorithm in a base class, allowing subclasses to override specific steps of the algorithm. In contrast, the Strategy pattern encapsulates entire algorithms as separate classes, making them interchangeable at runtime.

Command Design Pattern:

The Command Design Pattern encapsulates a request as an object, which allows for parameterizing clients with different requests. While the Strategy pattern also encapsulates behavior, it focuses on interchangeable algorithms rather than commands.

It’s important to choose the appropriate pattern based on the specific requirements and design goals of your application.

Use Case 1: Implementing a Payment Processing System

A common use case for the Strategy Design Pattern is implementing a payment processing system that supports multiple payment gateways. Each payment gateway may have its own authentication, authorization, and transaction processing logic.

Example:

public interface PaymentGateway {
    void authenticate();
    void authorize();
    void processTransaction();
}

public class PayPalGateway implements PaymentGateway {
    public void authenticate() {
        // PayPal authentication logic
    }

    public void authorize() {
        // PayPal authorization logic
    }

    public void processTransaction() {
        // PayPal transaction processing logic
    }
}

public class StripeGateway implements PaymentGateway {
    public void authenticate() {
        // Stripe authentication logic
    }

    public void authorize() {
        // Stripe authorization logic
    }

    public void processTransaction() {
        // Stripe transaction processing logic
    }
}

public class PaymentProcessor {
    private PaymentGateway gateway;

    public PaymentProcessor(PaymentGateway gateway) {
        this.gateway = gateway;
    }

    public void processPayment() {
        gateway.authenticate();
        gateway.authorize();
        gateway.processTransaction();
    }
}

public class Client {
    public static void main(String[] args) {
        PaymentProcessor processor = new PaymentProcessor(new PayPalGateway());
        processor.processPayment();

        processor = new PaymentProcessor(new StripeGateway());
        processor.processPayment();
    }
}

In this example, the PaymentGateway interface defines the contract for different payment gateways. The PayPalGateway and StripeGateway classes implement this interface and provide their own authentication, authorization, and transaction processing logic. The PaymentProcessor class acts as a client and delegates the payment processing to the selected gateway.

Related Article: How to Implement Recursion in Java

Use Case 2: Creating Different Sorting Algorithms

Another use case for the Strategy Design Pattern is creating different sorting algorithms that can be easily switched or combined based on specific requirements.

Example:

public interface SortingStrategy {
    void sort(int[] numbers);
}

public class BubbleSort implements SortingStrategy {
    public void sort(int[] numbers) {
        // Bubble sort implementation
    }
}

public class QuickSort implements SortingStrategy {
    public void sort(int[] numbers) {
        // Quick sort implementation
    }
}

public class MergeSort implements SortingStrategy {
    public void sort(int[] numbers) {
        // Merge sort implementation
    }
}

public class SortingAlgorithm {
    private List<SortingStrategy> strategies;

    public SortingAlgorithm() {
        strategies = new ArrayList<>();
    }

    public void addStrategy(SortingStrategy strategy) {
        strategies.add(strategy);
    }

    public void sortNumbers(int[] numbers) {
        for (SortingStrategy strategy : strategies) {
            strategy.sort(numbers);
        }
    }
}

public class Client {
    public static void main(String[] args) {
        int[] numbers = {5, 1, 3, 2, 4};

        SortingAlgorithm algorithm = new SortingAlgorithm();
        algorithm.addStrategy(new BubbleSort());
        algorithm.addStrategy(new QuickSort());
        algorithm.addStrategy(new MergeSort());

        algorithm.sortNumbers(numbers);
    }
}

In this example, the SortingStrategy interface defines the contract for different sorting algorithms. The BubbleSort, QuickSort, and MergeSort classes implement this interface and provide their respective sorting logic. The SortingAlgorithm class manages a collection of strategies and applies them sequentially to sort an array of numbers.

Best Practices for Implementing the Strategy Design Pattern

When implementing the Strategy Design Pattern, consider the following best practices:

1. Identify the varying behavior: Determine the parts of your codebase that exhibit varying behavior and are candidates for encapsulation as strategies.

2. Define a common interface or abstract class: Create an interface or abstract class that defines the contract for all strategies. This ensures that each strategy adheres to a consistent API.

3. Implement concrete strategies: Create concrete classes that implement the interface or extend the abstract class, providing the specific behavior for each strategy.

4. Encapsulate the strategy selection logic: Define a class that encapsulates the strategy selection and management. This class should allow clients to set or change the strategy at runtime.

5. Favor composition over inheritance: Instead of using inheritance to switch behavior, use composition to encapsulate behavior and make it interchangeable.

6. Test strategies independently: Test each strategy implementation in isolation to ensure correctness and verify that they produce the expected results.

7. Separate strategy-specific logic from generic logic: Avoid mixing strategy-specific code with general-purpose code. This promotes better organization and maintainability.

Real World Example: Creating a Text Editor with Multiple Formatting Options

A real-world example of the Strategy Design Pattern is creating a text editor with multiple formatting options. The text editor may support different formatting strategies such as bold, italic, underline, and strikethrough.

Example:

public interface TextFormattingStrategy {
    String format(String text);
}

public class BoldFormatting implements TextFormattingStrategy {
    public String format(String text) {
        return "<b>" + text + "</b>";
    }
}

public class ItalicFormatting implements TextFormattingStrategy {
    public String format(String text) {
        return "<i>" + text + "</i>";
    }
}

public class UnderlineFormatting implements TextFormattingStrategy {
    public String format(String text) {
        return "<u>" + text + "</u>";
    }
}

public class TextEditor {
    private TextFormattingStrategy formattingStrategy;

    public void setFormattingStrategy(TextFormattingStrategy formattingStrategy) {
        this.formattingStrategy = formattingStrategy;
    }

    public String formatText(String text) {
        return formattingStrategy.format(text);
    }
}

public class Client {
    public static void main(String[] args) {
        TextEditor editor = new TextEditor();

        editor.setFormattingStrategy(new BoldFormatting());
        String formattedText = editor.formatText("Hello World");
        System.out.println(formattedText);

        editor.setFormattingStrategy(new ItalicFormatting());
        formattedText = editor.formatText("Hello World");
        System.out.println(formattedText);

        editor.setFormattingStrategy(new UnderlineFormatting());
        formattedText = editor.formatText("Hello World");
        System.out.println(formattedText);
    }
}

In this example, the TextFormattingStrategy interface defines the contract for different formatting options. The BoldFormatting, ItalicFormatting, and UnderlineFormatting classes implement this interface and provide their respective formatting logic. The TextEditor class allows the client to set the formatting strategy and apply it to format the text accordingly.

Related Article: Java Adapter Design Pattern Tutorial

Performance Considerations for the Strategy Design Pattern

When using the Strategy Design Pattern, it’s important to consider potential performance implications. Here are a few considerations:

1. Strategy selection overhead: There may be some overhead in selecting and setting the appropriate strategy at runtime. However, this overhead is typically negligible compared to the benefits of flexibility and maintainability.

2. Strategy instantiation: Depending on the complexity of the strategies, creating strategy instances may introduce additional overhead. Consider using object pooling or other techniques to mitigate this, if necessary.

3. Strategy-specific optimizations: Strategies may have different performance characteristics. It’s important to analyze and optimize each strategy independently to ensure optimal performance.

4. Caching and memoization: If strategies involve expensive calculations or computations, consider caching or memoizing the results to avoid redundant calculations.

Advanced Technique 1: Dynamic Strategy Selection

In some cases, you may need to dynamically select the strategy at runtime based on certain conditions or user input. One way to achieve dynamic strategy selection is by using a factory or a registry that maps conditions to corresponding strategies.

Example:

public class StrategyFactory {
    private static Map<String, TextFormattingStrategy> strategies = new HashMap<>();

    static {
        strategies.put("bold", new BoldFormatting());
        strategies.put("italic", new ItalicFormatting());
        strategies.put("underline", new UnderlineFormatting());
    }

    public static TextFormattingStrategy getStrategy(String condition) {
        return strategies.get(condition);
    }
}

public class TextEditor {
    private TextFormattingStrategy formattingStrategy;

    public void setFormattingStrategy(TextFormattingStrategy formattingStrategy) {
        this.formattingStrategy = formattingStrategy;
    }

    public String formatText(String text) {
        return formattingStrategy.format(text);
    }
}

public class Client {
    public static void main(String[] args) {
        TextEditor editor = new TextEditor();

        String condition = "bold"; // Condition obtained from user input or other sources
        TextFormattingStrategy strategy = StrategyFactory.getStrategy(condition);
        editor.setFormattingStrategy(strategy);

        String formattedText = editor.formatText("Hello World");
        System.out.println(formattedText);
    }
}

In this example, the StrategyFactory class acts as a factory that maps conditions to corresponding strategies. The TextEditor class still uses the setFormattingStrategy method to set the strategy dynamically based on the condition obtained from the user or other sources.

Advanced Technique 2: Implementing Strategy Families

In some scenarios, you may need to group related strategies into families and provide a way to select a strategy from a specific family. One approach is to introduce an additional level of abstraction by using abstract factories or abstract strategy families.

Example:

public interface TextFormattingStrategy {
    String format(String text);
}

public interface TextFormattingFactory {
    TextFormattingStrategy createFormattingStrategy();
}

public class BoldFormattingFactory implements TextFormattingFactory {
    public TextFormattingStrategy createFormattingStrategy() {
        return new BoldFormatting();
    }
}

public class ItalicFormattingFactory implements TextFormattingFactory {
    public TextFormattingStrategy createFormattingStrategy() {
        return new ItalicFormatting();
    }
}

public class TextEditor {
    private TextFormattingStrategy formattingStrategy;

    public void setFormattingStrategy(TextFormattingFactory factory) {
        formattingStrategy = factory.createFormattingStrategy();
    }

    public String formatText(String text) {
        return formattingStrategy.format(text);
    }
}

public class Client {
    public static void main(String[] args) {
        TextEditor editor = new TextEditor();

        TextFormattingFactory factory = new BoldFormattingFactory();
        editor.setFormattingStrategy(factory);

        String formattedText = editor.formatText("Hello World");
        System.out.println(formattedText);
    }
}

In this example, the TextFormattingFactory interface defines the contract for creating specific formatting strategies. The BoldFormattingFactory and ItalicFormattingFactory classes implement this interface and create instances of the corresponding strategies. The TextEditor class now accepts a factory instead of a strategy directly, allowing for dynamic selection of strategy families.

Related Article: How to Print a Hashmap in Java

Advanced Technique 3: Using Dependency Injection with the Strategy Design Pattern

The Strategy Design Pattern can be effectively used in conjunction with Dependency Injection (DI) frameworks. DI allows for the decoupling of strategy dependencies from the client code, making it easier to manage and configure strategies.

Example (using Spring Framework for DI):

public interface TextFormattingStrategy {
    String format(String text);
}

@Component
public class BoldFormatting implements TextFormattingStrategy {
    public String format(String text) {
        return "<b>" + text + "</b>";
    }
}

@Component
public class ItalicFormatting implements TextFormattingStrategy {
    public String format(String text) {
        return "<i>" + text + "</i>";
    }
}

@Component
public class TextEditor {
    private TextFormattingStrategy formattingStrategy;

    @Autowired
    public void setFormattingStrategy(TextFormattingStrategy formattingStrategy) {
        this.formattingStrategy = formattingStrategy;
    }

    public String formatText(String text) {
        return formattingStrategy.format(text);
    }
}

public class Client {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        TextEditor editor = context.getBean(TextEditor.class);

        String formattedText = editor.formatText("Hello World");
        System.out.println(formattedText);
    }
}

In this example, the TextFormattingStrategy interface is implemented by the BoldFormatting and ItalicFormatting classes, which are annotated as Spring components. The TextEditor class uses the @Autowired annotation to inject the appropriate formatting strategy into the formattingStrategy field.

Code Snippet 1: Implementing a Strategy Interface

public interface SortingStrategy {
    void sort(int[] numbers);
}

This code snippet demonstrates the implementation of a strategy interface for sorting algorithms. The SortingStrategy interface defines a contract for different sorting strategies, requiring them to implement a sort method that takes an array of integers as input.

Code Snippet 2: Creating Concrete Strategy Classes

public class BubbleSort implements SortingStrategy {
    public void sort(int[] numbers) {
        // Bubble sort implementation
    }
}

public class QuickSort implements SortingStrategy {
    public void sort(int[] numbers) {
        // Quick sort implementation
    }
}

This code snippet provides two concrete classes, BubbleSort and QuickSort, that implement the SortingStrategy interface. Each class provides its own implementation of the sort method, representing different sorting algorithms.

Related Article: How to Manage Collections Without a SortedList in Java

Code Snippet 3: Invoking the Strategy Pattern in Client Code

int[] numbers = {5, 1, 3, 2, 4};

SortingStrategy strategy = new BubbleSort();
strategy.sort(numbers);

strategy = new QuickSort();
strategy.sort(numbers);

This code snippet demonstrates how the strategy pattern is invoked in client code. The client creates an instance of a specific strategy, such as BubbleSort or QuickSort, and invokes the sort method on that strategy. The strategy performs the sorting algorithm on the given array of numbers.

Code Snippet 4: Error Handling in the Strategy Design Pattern

public interface SortingStrategy {
    void sort(int[] numbers) throws SortingException;
}

public class BubbleSort implements SortingStrategy {
    public void sort(int[] numbers) throws SortingException {
        try {
            // Bubble sort implementation
        } catch (Exception e) {
            throw new SortingException("Error occurred during sorting", e);
        }
    }
}

public class QuickSort implements SortingStrategy {
    public void sort(int[] numbers) throws SortingException {
        try {
            // Quick sort implementation
        } catch (Exception e) {
            throw new SortingException("Error occurred during sorting", e);
        }
    }
}

public class SortingException extends Exception {
    public SortingException(String message, Throwable cause) {
        super(message, cause);
    }
}

This code snippet demonstrates how error handling can be incorporated into the strategy pattern. The SortingStrategy interface now declares that the sort method throws a custom SortingException. Each concrete strategy class catches any exceptions that occur during sorting and rethrows them as SortingException. This allows clients to handle any potential errors that may occur during the sorting process.

Code Snippet 5: Applying Strategy Design Pattern in Multithreaded Environments

public class SortingThread extends Thread {
    private SortingStrategy strategy;
    private int[] numbers;

    public SortingThread(SortingStrategy strategy, int[] numbers) {
        this.strategy = strategy;
        this.numbers = numbers;
    }

    public void run() {
        strategy.sort(numbers);
    }
}

public class Client {
    public static void main(String[] args) {
        int[] numbers1 = {5, 1, 3, 2, 4};
        int[] numbers2 = {9, 7, 6, 8, 10};

        SortingStrategy strategy = new BubbleSort();

        SortingThread thread1 = new SortingThread(strategy, numbers1);
        SortingThread thread2 = new SortingThread(strategy, numbers2);

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

This code snippet demonstrates how the Strategy Design Pattern can be applied in a multithreaded environment. The SortingThread class extends Thread and takes a sorting strategy and an array of numbers as input. Each thread runs the sorting strategy on its respective array of numbers. The Client class creates two sorting threads and starts them concurrently. The join method is used to ensure that both threads complete before proceeding.

Java String Comparison: String vs StringBuffer vs StringBuilder

This article How to choose the right string manipulation approach in Java: String, StringBuffer, or StringBuilder. This article provides an overview of String,... read more

How to Fix the Java NullPointerException

Java's NullPointerException is a common error that many developers encounter. In this tutorial, you will learn how to handle this error effectively. The article covers... read more

How to Convert a String to an Array in Java

This article provides a simple tutorial on converting a String to an Array in Java. It covers various methods and best practices for string to array conversion, as well... read more

How To Fix Java Certification Path Error

Ensure your web applications are secure with this article. Learn how to resolve the 'unable to find valid certification path to requested target' error in Java with... read more