Discover Cloud Solutions with HostingerGet a special discount.

hostingerLearn More
Published on

Strategy Design Pattern in Java

Authors
  • avatar
    Name
    Luis Carbonel
    Twitter

In this post, we will explore in depth the Strategy Design Pattern and its application in Java. Use this powerful pattern to optimize strategies in your code through a practical example, and discover how it enhances flexibility and maintainability. Let’s dive into the world of strategic programming!

Strategy Design Pattern in Java

What is the Strategy Design Pattern?

The Strategy pattern is one of the most widely used design patterns in software development. It belongs to the category of behavioral patterns and focuses on separating specific algorithms or strategies from the main code, thus allowing greater flexibility and ease of maintenance. If this seems a bit confusing to you, don’t worry, we will understand it better later on through our practical example of tax calculation.

The idea behind the Strategy Design Pattern

The Strategy pattern gives us the ability to change the behavior of an object at runtime, without having to modify the existing code or affect the overall structure of the application.

Up to this point, the theory is alluring, but how do we take these concepts to our code? What is the idea behind this pattern in practice? Well, the idea is to have a standard interface for all strategies and encapsulate each strategy in a concrete class that implements that interface.

Next, we will start with a requirement that usually appears when we develop in enterprise environments.

The problem to solve with the Strategy Design Pattern

Tax Calculation System

Imagine you get a requirement where you need to develop a tax calculation system for an accounting application. The system must be able to calculate different types of taxes according to the specific rules of each type. The types of taxes to be considered are income tax, value-added tax (VAT), and sales tax.

Business rules for each tax type

The business rules for each tax type are as follows:

  • Income tax is calculated by applying a flat rate of 20% on the amount of income.
  • Value-added tax (VAT) is calculated by applying a flat rate of 15% on the amount of income.
  • Sales tax is calculated by applying a flat rate of 10% on the amount of income.

Our system must allow us to calculate the total tax for a set of quantities entered. Each amount must be processed and the corresponding tax must be calculated according to the selected tax rate.

In addition, the system must be flexible to allow the incorporation of new tax types in the future, with specific calculation rules.

Tax Calculation solution with Strategy Design Pattern in Java

Set method to change the tax calculation strategy at runtime

Next, we will define the classes involved and explain in detail how the behavior is. Let’s rub our hands together because we will start coding our program.

First, we will define an interface called TaxCalculator that will be implemented by all concrete classes that represent the different tax calculation strategies.

public interface TaxCalculator {
    double calculateTax(double amount);
}

The calculateTax method will be responsible for calculating the tax for a given amount.

Next, we will define the concrete classes that implement the TaxCalculator interface.

public class IncomeTaxCalculator implements TaxCalculator {

    public static final double FIX_RATE_PERCENTAGE = 0.2;

    @Override
    public double calculateTax(double amount) {
        // Income Tax Calculation Logic.
        // Fixed rate of 20% for income tax
        return FIX_RATE_PERCENTAGE * amount;
    }
}
public class VATCalculator implements TaxCalculator {

    public static final double FIX_RATE_PERCENTAGE = 0.15;

    @Override
    public double calculateTax(double amount) {
        // VAT Calculation Logic.
        // Fixed rate of 15% for VAT
        return FIX_RATE_PERCENTAGE * amount;
    }
}
public class SalesTaxCalculator implements TaxCalculator {

        public static final double FIX_RATE_PERCENTAGE = 0.1;

        @Override
        public double calculateTax(double amount) {
            // Sales Tax Calculation Logic.
            // Fixed rate of 10% for sales tax
            return FIX_RATE_PERCENTAGE * amount;
        }
}

As you can see, each concrete class implements the calculateTax method according to the specific calculation rules of each tax type.

Next, we will define the AccountingApplication class, which represents the accounting application that uses the Strategy pattern to calculate taxes. It has an attribute taxCalculator of type TaxCalculator, which is used to store the current tax calculation strategy.

public class AccountingApplication {

        private TaxCalculator taxCalculator;

        public AccountingApplication(TaxCalculator taxCalculator) {
            this.taxCalculator = taxCalculator;
        }

        public double calculateTax(double[] amounts) {
            // Tax Calculation Logic.
            // For each amount, calculate tax and add to total tax
            return Arrays.stream(amounts)
                    .map(taxCalculator::calculateTax)
                    .sum();
        }

        public void setTaxCalculator(TaxCalculator taxCalculator) {
            this.taxCalculator = taxCalculator;
        }
}
  • The AccountingApplication(TaxCalculator taxCalculator) constructor receives a tax calculation strategy and assigns it to the taxCalculator attribute.
  • The calculateTax(double[] amounts) method receives an array of amounts and calculates the total tax by adding the taxes calculated by the current strategy for each amount.
  • The setTaxCalculator(TaxCalculator taxCalculator) method is used to change the tax calculation strategy at runtime.

Finally, we will define the StrategyExample class, which is responsible for creating an instance of the AccountingApplication class and using it to calculate the total tax for a set of amounts.

public class StrategyExample {

    private static class AccountingApplication {

        private TaxCalculator taxCalculator;

        public AccountingApplication(TaxCalculator taxCalculator) {
            this.taxCalculator = taxCalculator;
        }

        public double calculateTax(double[] amounts) {
            // Tax Calculation Logic.
            // For each amount, calculate tax and add to total tax
            return Arrays.stream(amounts)
                    .map(taxCalculator::calculateTax)
                    .sum();
        }

        public void setTaxCalculator(TaxCalculator taxCalculator) {
            this.taxCalculator = taxCalculator;
        }
    }


        public static void main(String[] args) {
        double[] amounts = {100, 200, 300}; // Amounts to calculate tax for
        AccountingApplication accountingApplication = new AccountingApplication(new IncomeTaxCalculator());
        System.out.println("Total Income Tax: " + accountingApplication.calculateTax(amounts));

        accountingApplication.setTaxCalculator(new VATCalculator());
        System.out.println("Total VAT Tax: " + accountingApplication.calculateTax(amounts));

        accountingApplication.setTaxCalculator(new SalesTaxCalculator());
        System.out.println("Total Sales Tax: " + accountingApplication.calculateTax(amounts));
    }
}

In the main method, an instance of AccountingApplication is created with the strategy IncomeTaxCalculator, and the total tax is calculated for a set of amounts. Then, the strategy is changed to VATCalculator and the total tax is recalculated. Finally, the strategy is changed to SalesTaxCalculator and the total tax is calculated once again.

Now let’s try our small but illustrative application.

Total Income Tax: 120.0
Total VAT Tax: 90.0
Total Sales Tax: 60.0

By using the Strategy pattern in this code, you solve the problem of having to modify existing code every time you need to add a new tax type or change tax calculation rules. Instead of having multiple conditionals or control statements within the AccountingApplication class, the Strategy pattern approach is used to encapsulate each tax calculation strategy in a separate class. This allows you to easily add new tax strategies by simply implementing the TaxCalculator interface without affecting the rest of the system.

By changing the tax calculation strategy at runtime using the setTaxCalculator method, the accounting application can adapt to different tax types without needing to modify its main structure. This provides flexibility and facilitates code extensibility and maintainability.

Improving flexibility with a context and EnumMap

In this section, we will explore an enhancement to the implementation of the Strategy pattern that gives us even more flexibility and scalability in the handling of tax calculation strategies.

We introduce this TaxType enum class that defines the different types of taxes available in the system. In this case, three tax types are defined: SALES_TAX, INCOME_TAX and VAT_TAX.

public enum TaxType {
    SALES_TAX,
    INCOME_TAX,
    VAT_TAX
}

By using an enumeration, we ensure that only the predefined tax values can be used and avoid type errors or invalid values.

public interface TaxCalculator {
    double calculateTax(double amount);

    TaxType getTaxType();
}

The getTaxType() method has been added to the TaxCalculator interface. This method returns the tax type associated with the calculation strategy.

Next, we will define the concrete classes that implement the TaxCalculator interface.

public class IncomeTaxCalculator implements TaxCalculator {

    public static final double FIX_RATE_PERCENTAGE = 0.2;

    @Override
    public double calculateTax(double amount) {
        // Income Tax Calculation Logic.
        // Fixed rate of 20% for income tax
        return FIX_RATE_PERCENTAGE * amount;
    }

    @Override
    public TaxType getTaxType() {
        return TaxType.INCOME_TAX;
    }
}
public class VATCalculator implements TaxCalculator {

    public static final double FIX_RATE_PERCENTAGE = 0.15;

    @Override
    public double calculateTax(double amount) {
        // VAT Calculation Logic.
        // Fixed rate of 15% for VAT
        return FIX_RATE_PERCENTAGE * amount;
    }

    @Override
    public TaxType getTaxType() {
        return TaxType.VAT_TAX;
    }
}
public class SalesTaxCalculator implements TaxCalculator {

        public static final double FIX_RATE_PERCENTAGE = 0.1;

        @Override
        public double calculateTax(double amount) {
            // Sales Tax Calculation Logic.
            // Fixed rate of 10% for sales tax
            return FIX_RATE_PERCENTAGE * amount;
        }

    @Override
    public TaxType getTaxType() {
        return TaxType.SALES_TAX;
    }
}

As you can see, each concrete class implements the calculateTax method according to the specific calculation rules of each tax type. In addition, the getTaxType() method is implemented to return the tax type associated with the calculation strategy.

Next, we will define the AppContext class, which represents the context of the application and is responsible for creating the different tax calculation strategies and storing them in a Map.

class AppContext {

        private final EnumMap<TaxType, TaxCalculator> taxCalculatorStrategies = new EnumMap<>(TaxType.class);

        public AppContext() {
            initTaxCalculatorStrategies();
        }

        private void initTaxCalculatorStrategies() {
            TaxCalculator incomeTaxCalculator = new IncomeTaxCalculator();
            taxCalculatorStrategies.put(incomeTaxCalculator.getTaxType(), incomeTaxCalculator);

            TaxCalculator vatCalculator = new VATCalculator();
            taxCalculatorStrategies.put(vatCalculator.getTaxType(), vatCalculator);

            TaxCalculator salesTaxCalculator = new SalesTaxCalculator();
            taxCalculatorStrategies.put(salesTaxCalculator.getTaxType(), salesTaxCalculator);
        }

        public TaxCalculator getTaxCalculator(TaxType taxType) {
            return taxCalculatorStrategies.get(taxType);
        }
}

In the initTaxCalculatorStrategies() method, the tax calculation strategies (IncomeTaxCalculator, VATCalculator, and SalesTaxCalculator) are initialized and mapped to their respective tax types in the EnumMap.

The method getTaxCalculator(TaxType taxType) has been added which receives a tax type and returns the corresponding tax calculation strategy from the EnumMap.

Now, in our AccountingApplication the constructor now receives an AppContext instance instead of a tax calculation strategy directly.

    private static class AccountingApplication {
        private final AppContext appContext;

        public AccountingApplication(AppContext appContext) {
            this.appContext = appContext;
        }

        public double calculateTax(double[] amounts, TaxType taxType) {
            // Tax Calculation Logic.
            // For each amount, calculate tax and add to total tax based on taxType
            return Arrays.stream(amounts)
                    .map(amount -> appContext.getTaxCalculator(taxType).calculateTax(amount))
                    .sum();
        }
    }

In the calculateTax() method, a new taxType parameter has been added that indicates the type of tax for which we want to calculate the total. We use this parameter to get the corresponding tax calculation strategy from the context and apply it to each amount in the calculation.

Finally, the main method has been modified to use the new implementation.

public static void main(String[] args) {
    double[] amounts = {100, 200, 300}; // Amounts to calculate tax for
    AppContext appContext = new AppContext();
    AccountingApplication accountingApplication = new AccountingApplication(appContext);
    System.out.println("Total Income Tax: " + accountingApplication.calculateTax(amounts, TaxType.INCOME_TAX));
    System.out.println("Total VAT Tax: " + accountingApplication.calculateTax(amounts, TaxType.VAT_TAX));
    System.out.println("Total Sales Tax: " + accountingApplication.calculateTax(amounts, TaxType.SALES_TAX));
}

The calls to calculateTax() have been updated to include the taxType parameter corresponding to each type of tax.

Now let’s try our improved application.

Total Income Tax: 120.0
Total VAT Tax: 90.0
Total Sales Tax: 60.0

Now let’s take a look at a summary of the improvements introduced in the code.

  • With these changes, we have improved the structure and flexibility of our tax calculation system. Context and EnumMap now allow us to manage strategies in a more efficient and scalable way, and the getTaxType() method of the strategies allows us to easily identify the type of tax associated with each strategy.

Strategy Design Pattern without using interfaces and classes

In this section, we will see a different approach on how to apply the strategy design pattern without using interfaces and classes. We will introduce a few small changes to our example, and then we will see the implications of those changes. We will also discuss the advantages and disadvantages of this new implementation.

For this new implementation, we will only make use of the AppContext, AccountingApplication, and TaxType classes. The TaxCalculator interface and its implementations will not be needed.

private static class AppContext{
        private static final EnumMap<TaxType, Function<Double, Double>> TAX_CALCULATOR_STRATEGIES = new EnumMap<>(TaxType.class);

        private static final double FIX_RATE_PERCENTAGE_INCOME = 0.2;
        private static final double FIX_RATE_PERCENTAGE_VAT = 0.15;
        private static final double FIX_RATE_PERCENTAGE_SALES = 0.1;

        public AppContext() {
            initTaxCalculatorStrategies();
        }

        public Function<Double, Double> getCalculatorTax(TaxType taxType) {
            return TAX_CALCULATOR_STRATEGIES.get(taxType);
        }

        private static void initTaxCalculatorStrategies() {
            TAX_CALCULATOR_STRATEGIES.put(TaxType.INCOME_TAX, amount -> FIX_RATE_PERCENTAGE_INCOME * amount);
            TAX_CALCULATOR_STRATEGIES.put(TaxType.VAT_TAX,  amount -> FIX_RATE_PERCENTAGE_VAT * amount);
            TAX_CALCULATOR_STRATEGIES.put(TaxType.SALES_TAX, amount -> FIX_RATE_PERCENTAGE_SALES * amount);
        }
}

Let’s see what are the differences between the implementation with classes and the implementation with lambdas.

Implementation with Classes

  • The implementation with classes uses the TaxCalculator abstraction to represent each tax calculation strategy.
  • In the initTaxCalculatorStrategies() method, concrete instances of the IncomeTaxCalculator, VATCalculator and SalesTaxCalculator classes are created for each strategy and stored in an EnumMap.
  • The getTaxCalculator() method returns the concrete instance of TaxCalculator corresponding to the specified tax type.

Implementation with Lambdas

  • The implementation with lambdas uses lambda functions as a representation of each tax calculation strategy.
  • In the constructor, the initTaxCalculatorStrategies() method is invoked to initialize the strategies.
  • The getCalculatorTax() method returns the lambda function corresponding to the specified tax type.
  • In the initTaxCalculatorStrategies() method, the lambda functions are defined directly and stored in the EnumMap.

Core differences between the two implementations

  • Abstraction: In the implementation with classes, the TaxCalculator abstraction is used as a common interface for all strategies, while in the implementation with lambdas, lambda functions are used directly without the need for additional abstraction.
  • Instance creation: In both implementations, strategies are stored in an EnumMap. However, in the implementation with classes, instances of the concrete classes are created, while in the implementation with lambdas, they are directly defined as lambda functions.
  • Flexibility: The implementation with lambdas provides a more flexible and concise way of defining tax calculation strategies since it does not require the creation of additional classes and allows a more compact syntax.

In summary, the implementation with lambdas further simplifies the code by defining the strategies directly as lambda functions, avoiding the need to create additional classes. This provides a more concise and flexible implementation of the strategy design pattern in Java.

Now, let’s see how the AccountingApplication class changes in the implementation with lambdas.

 private static class AccountingApplication {
        private final AppContext appContext;

        public AccountingApplication(AppContext appContext) {
            this.appContext = appContext;
        }


        public double calculateTax(double[] amounts, TaxType taxType) {
            // Tax Calculation Logic.
            // For each amount, calculate tax and add to total tax based on taxType
            return  Arrays.stream(amounts)
                    .map(amount -> appContext.getCalculatorTax( taxType).apply(amount))
                    .reduce(0.0, Double::sum);
        }

    }

The calculateTax() method has been updated to use the apply() method of the lambda function corresponding to the specified tax type.

Finally, the main method has been modified to use the new implementation.

public static void main(String[] args) {
        double[] amounts = {100, 200, 300}; // Amounts to calculate tax for

        // Tax Calculation Logic.
        // For each amount, calculate tax and add to total tax based on taxType
        AppContext appContext = new AppContext();
        AccountingApplication accountingApplication = new AccountingApplication(appContext);
        System.out.println("Total Income Tax: " + accountingApplication.calculateTax(amounts, TaxType.INCOME_TAX));
        System.out.println("Total VAT Tax: " + accountingApplication.calculateTax(amounts, TaxType.VAT_TAX));
        System.out.println("Total Sales Tax: " + accountingApplication.calculateTax(amounts, TaxType.SALES_TAX));
    }

The calls to calculateTax() have been updated to include the taxType parameter corresponding to each type of tax.

Now let’s try our improved application.

Total Income Tax: 120.0
Total VAT Tax: 90.0
Total Sales Tax: 60.0

Advantages and disadvantages of lambdas in the Strategy Design Pattern

  • Code simplification: Using lambda functions makes it possible to reduce the number of classes and files needed to implement the strategies.
  • Greater conciseness: Lambda functions are shorter and more concise than creating separate classes, which makes the code easier to understand.
  • Flexibility: With lambda functions, it is possible to define and change strategies more dynamically, without the need to create new classes or modify existing ones.

Disadvantages of lambdas in the Strategy Design Pattern

  • Less readability: Although lambda functions can be more concise, their excessive use or poor organization can hinder code readability and comprehension.
  • Limitations in reuse: When using lambda functions, the reuse of specific strategies may be more complicated, since they cannot be instantiated and stored in variables for later use.

In general, the approach with lambda functions offers a more compact and flexible way to implement the strategy pattern in Java. However, it is important to consider the balance between code conciseness and readability, as well as the reusability of specific strategies in future cases.

Conclusion

The Strategy design pattern allows us to change the behavior of an object at runtime without modifying the existing code or affecting the structure of the application. In tax calculation, we apply this pattern to have different calculation strategies and change them as needed.

Initially, we implemented the tax calculation strategies as separate classes that implemented a common interface. This allowed us to change the strategy at runtime, but required modifications to the main class each time a new strategy was added.

We then improved the design by using a context and enum map to store and access tax calculation strategies. This gave us more flexibility and extensibility, since we could add new strategies without modifying the main class.

In summary, the Strategy pattern is a powerful tool for managing variant behaviors in a system. By applying it correctly, we achieve a more flexible, scalable and maintainable design. In the case of tax calculation, this pattern allows us to adapt to changes in tax policies, add new strategies and maintain a clean and modular code.

I hope this article has provided you with a clear understanding of the Strategy design pattern and its application in Java. Use this pattern wisely and take your design to a new level.

All the code snippets mentioned in the article can be found on GitHub