Implementing the Strategy Pattern using Lambda Expressions in Java 8

In this tutorial, we will explore how to implement the Strategy Pattern in Java 8 using lambda expressions, which will significantly simplify our code and make it more concise.

Table of Content:

Understanding Strategy Pattern

The Strategy Pattern is a behavioral design pattern that focuses on encapsulating a family of algorithms, making them interchangeable, and allowing clients to select the algorithm that best suits their needs at runtime. This pattern provides a way to define a set of algorithms independently from the client code that uses them, promoting flexibility, extensibility, and maintainability in software design.

Intent

The primary intent of the Strategy Pattern is to:

  1. Define a family of algorithms: Identify a specific behavior or algorithm that can vary within a software system. These algorithms are encapsulated within separate classes, each representing a different strategy.
  2. Encapsulate each algorithm: Create concrete classes that implement the algorithm interface, which ensures that all strategies adhere to a consistent contract. This encapsulation allows strategies to be changed or extended without affecting the client code.
  3. Make strategies interchangeable: Provide a way to switch between different strategies at runtime, allowing clients to choose the appropriate behavior based on specific requirements or conditions.

Structure

The key components of the Strategy Pattern are:

  1. Context: This is the class that utilizes the strategy. It maintains a reference to the selected strategy object and delegates the execution of the algorithm to that strategy.
  2. Strategy: The interface or abstract class that declares the contract for all concrete strategies. It defines the method(s) that encapsulate the algorithm.
  3. Concrete Strategies: These are the concrete classes that implement the strategy interface. Each class provides a specific implementation of the algorithm.

Benefits

The Strategy Pattern offers several advantages:

  1. Flexibility: Clients can easily switch between different strategies without modifying the context or other strategies. New strategies can be added without altering existing code.
  2. Code Reusability: Strategies encapsulate specific behavior, making it easy to reuse them in different parts of the application.
  3. Readability and Maintainability: Each strategy is self-contained and easy to understand. Changes to one strategy do not impact others, contributing to a maintainable codebase.
  4. Open/Closed Principle: The pattern adheres to the Open/Closed Principle, as new strategies can be added without modifying existing code.
  5. Testability: Strategies can be tested independently, promoting better unit testing.

Use Cases

The Strategy Pattern is suitable when:

  • A class has multiple variants of an algorithm, and the client needs the flexibility to choose one of them at runtime.
  • There is a need to isolate and encapsulate specific behaviors, promoting code organization and separation of concerns.
  • Different variations of an algorithm are required across different parts of the application.

Defining the Strategy Interface for Strategy Pattern

In the Strategy Pattern, we first need to define a strategy interface that declares the algorithm’s method(s). Let’s create an example interface representing a payment strategy:

@FunctionalInterface
interface PaymentStrategy {
    void pay(int amount);
}

The @FunctionalInterface annotation is optional but serves as a reminder to enforce the interface’s single abstract method constraint.

Implementing Strategies using Lambda Expressions

Next, we’ll create different payment strategies using lambda expressions. Each lambda expression represents an implementation of the pay method in the PaymentStrategy interface:

public class PaypalPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        // Implementation of Paypal payment logic
        System.out.println("Paid " + amount + " using Paypal.");
    }
}

public class CreditCardPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        // Implementation of credit card payment logic
        System.out.println("Paid " + amount + " using Credit Card.");
    }
}

// Lambda expressions for payment strategies
PaymentStrategy applePay = amount -> System.out.println("Paid " + amount + " using Apple Pay.");
PaymentStrategy googlePay = amount -> System.out.println("Paid " + amount + " using Google Pay.");

Creating the Context Class for Strategy Pattern

The Context class maintains a reference to the selected payment strategy and delegates the payment logic to it:

public class PaymentContext {
    private PaymentStrategy paymentStrategy;

    public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

    public void makePayment(int amount) {
        paymentStrategy.pay(amount);
    }
}

Putting It All Together

Now, let’s use the strategies we created earlier with the context class:

public class Main {
    public static void main(String[] args) {
        PaymentContext context = new PaymentContext();

        // Set the payment strategy to Paypal
        context.setPaymentStrategy(new PaypalPayment());
        context.makePayment(100);

        // Set the payment strategy to Credit Card
        context.setPaymentStrategy(new CreditCardPayment());
        context.makePayment(200);

        // Set the payment strategy to Apple Pay using lambda expression
        context.setPaymentStrategy(applePay);
        context.makePayment(50);

        // Set the payment strategy to Google Pay using lambda expression
        context.setPaymentStrategy(googlePay);
        context.makePayment(70);
    }
}

Output

Paid 100 using Paypal.
Paid 200 using Credit Card.
Paid 50 using Apple Pay.
Paid 70 using Google Pay.

In this tutorial, we successfully implemented the Strategy Pattern in Java 8 using lambda expressions. By doing so, we achieved more concise and flexible code. The Strategy Pattern promotes better code organization and easier addition of new payment strategies without modifying the existing codebase. This approach allows us to follow the Open/Closed Principle, one of the key principles of object-oriented design.

Leave a Reply

Your email address will not be published. Required fields are marked *

Post comment