DEV Community

Cover image for Strategy Design Pattern in Java
Matheus Bernardes Spilari
Matheus Bernardes Spilari

Posted on

Strategy Design Pattern in Java

The Strategy Design Pattern is a behavioral design pattern that allows a class's behavior to be selected at runtime. It is particularly useful when you want to define a family of algorithms, encapsulate each one in its class, and make them interchangeable.

In this article, we will explore:

  • What the Strategy Pattern is and why it’s useful.
  • How code looks without the Strategy Pattern.
  • How refactoring with the Strategy Pattern improves flexibility and adheres to SOLID principles.
  • Two practical implementations: a calculator and a payment system.

What Is the Strategy Design Pattern?

The Strategy Pattern defines a way to create interchangeable families of algorithms by encapsulating them in separate classes and using a common interface. The client class delegates the algorithm's execution to the specific strategy it chooses.

This approach:

  • Encourages separation of concerns.
  • Makes adding new algorithms (strategies) straightforward.
  • Reduces the risk of introducing bugs when extending functionality.

Example 1: A Calculator Without the Strategy Pattern

Let's first look at a calculator application that performs addition, subtraction, multiplication, and division without the Strategy Pattern:

public class Calculator {
    private final double firstValue;
    private final double secondValue;
    private final String operation;

    public Calculator(double firstValue, double secondValue, String operation) {
        this.firstValue = firstValue;
        this.secondValue = secondValue;
        this.operation = operation;
    }

    public void result() {
        switch (operation) {
            case "sum":
                System.out.println("The result is: " + (firstValue + secondValue));
                break;
            case "subtraction":
                System.out.println("The result is: " + (firstValue - secondValue));
                break;
            case "multiply":
                System.out.println("The result is: " + (firstValue * secondValue));
                break;
            case "division":
                if (secondValue == 0) {
                    System.out.println("Error: Division by zero.");
                } else {
                    System.out.println("The result is: " + (firstValue / secondValue));
                }
                break;
            default:
                System.out.println("Invalid operation.");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Drawbacks of This Approach

  1. Violates the Open/Closed Principle: Adding a new operation requires modifying the result method, which increases the risk of introducing bugs.
  2. Hard to Extend or Test: Testing individual operations is harder because they’re tightly coupled within the Calculator class.
  3. Poor Maintainability: The switch statement grows with every new operation, making the code harder to read and maintain.

Refactoring with the Strategy Pattern

By applying the Strategy Pattern, we encapsulate each operation (addition, subtraction, etc.) into its own class, adhering to the Single Responsibility Principle. Here’s how the refactored calculator looks:

// Strategy Interface
public interface OperationStrategy {
    double execute(double firstValue, double secondValue);
}

// Concrete Strategies
public class AdditionStrategy implements OperationStrategy {
    @Override
    public double execute(double firstValue, double secondValue) {
        return firstValue + secondValue;
    }
}

public class SubtractionStrategy implements OperationStrategy {
    @Override
    public double execute(double firstValue, double secondValue) {
        return firstValue - secondValue;
    }
}

// ... MultiplyStrategy and DivisionStrategy similar to above

// Context Class
import java.util.Map;

public class Calculator {
    private final double firstValue;
    private final double secondValue;
    private final String operation;
    private final Map<String, OperationStrategy> mapStrategies = Map.of(
        "sum", new AdditionStrategy(),
        "subtraction", new SubtractionStrategy(),
        "multiply", new MultiplyStrategy(),
        "division", new DivisionStrategy()
    );

    public Calculator(double firstValue, double secondValue, String operation) {
        this.firstValue = firstValue;
        this.secondValue = secondValue;
        this.operation = operation;
    }

    public void result() {
        OperationStrategy operationStrategy = mapStrategies.get(operation);
        if (operationStrategy == null) {
            System.out.println("The " + operation + " is invalid");
            return;
        }
        double result = operationStrategy.execute(firstValue, secondValue);
        System.out.println("The " + operation + " between " + firstValue + " and " + secondValue + " is " + result);
    }
}
Enter fullscreen mode Exit fullscreen mode

Benefits

  • Open/Closed Principle: Adding a new operation requires creating a new strategy class rather than modifying existing code.
  • Single Responsibility Principle: Each operation is encapsulated in its own class, making it easier to test and maintain.
  • Flexibility: Changing or extending functionality becomes trivial by modifying the mapStrategies mapping.

Example 2: Payment System Without the Strategy Pattern

Now, let’s look at a simple payment system without the Strategy Pattern:

public class PaymentProcessor {
    private final double amount;
    private final String type;

    public PaymentProcessor(double amount, String type) {
        this.amount = amount;
        this.type = type;
    }

    public void totalCost() {
        switch (type) {
            case "money":
                System.out.println("Paid " + amount + " using Money.");
                break;
            case "paypal":
                System.out.println("Paid " + amount + " using PayPal.");
                break;
            case "creditcard":
                System.out.println("Paid " + amount + " using Credit Card.");
                break;
            default:
                System.out.println("Payment type not supported.");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Issues

  • Similar to the calculator example, this violates the Open/Closed Principle.
  • Adding new payment methods involves editing the switch statement, increasing complexity.

Refactoring the Payment System with the Strategy Pattern

Here’s how the payment system improves with the Strategy Pattern:

// Strategy Interface
public interface PaymentStrategy {
    double pay(double amount);
}

// Concrete Strategies
public class Money implements PaymentStrategy {
    @Override
    public double pay(double amount) {
        return amount;
    }
}

public class PayPal implements PaymentStrategy {
    @Override
    public double pay(double amount) {
        return amount;
    }
}

public class CreditCard implements PaymentStrategy {
    @Override
    public double pay(double amount) {
        return amount;
    }
}

// Context Class
import java.util.Map;

public class PaymentProcessor {
    private final double amount;
    private final String type;
    private final Map<String, PaymentStrategy> mapStrategies = Map.of(
        "money", new Money(),
        "paypal", new PayPal(),
        "creditcard", new CreditCard()
    );

    public PaymentProcessor(double amount, String type) {
        this.amount = amount;
        this.type = type;
    }

    public void totalCost() {
        PaymentStrategy strategy = mapStrategies.get(type);
        if (strategy == null) {
            System.out.println("The " + type + " method of payment isn't allowed.");
            return;
        }
        double result = strategy.pay(amount);
        System.out.println("Paid " + result + " using " + type + ".");
    }
}
Enter fullscreen mode Exit fullscreen mode

Advantages of Using the Strategy Pattern

  1. Adherence to SOLID Principles:
    • Single Responsibility Principle: Each strategy is responsible for its own behavior.
    • Open/Closed Principle: Strategies are easily extendable without modifying existing code.
  2. Testability: Each strategy can be tested independently, simplifying debugging.
  3. Flexibility and Scalability: Adding new algorithms or payment methods requires minimal changes.

Potential Downsides

  • Overhead for Small Applications: For simple systems, the additional complexity of multiple classes might be overkill.
  • Dependency Management: If strategies rely on shared dependencies, you might need to inject those dependencies properly, adding complexity.

Conclusion

The Strategy Design Pattern is a powerful tool for creating maintainable, scalable, and flexible code. By encapsulating behaviors in separate classes and adhering to SOLID principles, you can reduce coupling and improve the testability of your applications.

Use the Strategy Pattern when:

  • You have multiple variations of an algorithm.
  • You need to switch algorithms dynamically.
  • You want to make your system easy to extend without modifying existing code.

By refactoring the calculator and payment system, we've seen firsthand how the Strategy Pattern simplifies code and prepares it for future growth. Happy coding!


📍 Reference

💻 Project Repository

👋 Talk to me

Top comments (0)