Design patterns: strategy and factory patterns


This is the second article in my series on design models. In the first, we looked at the Builder model. We also briefly discussed the benefits of models. If you haven’t read it yet, it might be a good idea to review the first two paragraphs before continuing with this article.

When I sat down to start planning my next post, I was really torn between Strategy and Factory models. I have used both wisely in my code in the past and believe that both are fundamental patterns that are part of every OO developer’s vocabulary. It turns out that the Factory model complements the Strategy model quite well, so I decided to cover both in one article. As was the case with the Builder pattern we looked at last time, the Factory pattern is a creation pattern. The strategy model, on the other hand, is a behavior model.

The problem

As before, we will pretend that we are part of a team of Java developers working for a bank. This time, we are calculating the monthly interest on different types of bank accounts. Initially, we only deal with two types of accounts: current accounts paying 2% interest per year and savings accounts paying 4% per year. Interest will not apply to any other type of account. Our types of accounts are defined by an enumeration.

enum AccountTypes {CURRENT, SAVINGS}

Based on these types of accounts, we write an InterestCalculator class.

public class InterestCalculator {

    public double calculateInterest(AccountTypes accountType, double accountBalance) {
        switch (accountType) {
            case CURRENT: return accountBalance * (0.02 / 12);  //Monthly interest rate is annual rate / 12 months.
            case SAVINGS: return accountBalance * (0.04 / 12);
            default:
                return 0;
        }
    }
}


Our next requirement is to add support for two different money market accounts: a standard money market account paying 5% per annum and a special high-roller money market account that pays 7.5%, but only if the customer maintains a minimum balance. from R100,000.00. We modify our calculator accordingly.

public class InterestCalculator {

    public double calculateInterest(AccountTypes accountType, double accountBalance) {
        switch (accountType) {
            case CURRENT: return accountBalance * (0.02 / 12);  //Monthly interest rate is annual rate / 12 months.
            case SAVINGS: return accountBalance * (0.04 / 12);
            case STANDARD_MONEY_MARKET: return accountBalance * (0.06/12);
            case HIGH_ROLLER_MONEY_MARKET: return accountBalance < 100000.00 ? 0 : accountBalance * (0.075/12);
            default:
                return 0;
        }
    }
}

It should be obvious that our code gets more complicated with each set of new requirements that we implement. We have all of these business rules bundled into a class that is getting harder and harder to understand. Additionally, rumor has it that the bank’s asset finance department has heard of our new interest calculator and would like to use it to calculate interest on customer loans. However, their interest rates aren’t fixed – they’re tied to a central bank’s interest rates, which we’ll have to retrieve through a web service. Not only are we starting to deal with more and more types of accounts, but the calculation logic is also becoming more and more complex.

If we keep adding more and more business rules to our calculator, we’re going to end up with something that could become very difficult to maintain. Of course, we can try to extract each calculation in its own method, which could be a bit cleaner, but ultimately it will always be lipstick on a pig.

The problem we have is the following:

  • We have a unique, convoluted, and difficult to maintain method that tries to deal with a number of different scenarios.

The strategy model can help us solve this problem.

The models)

The Strategy model allows us to dynamically exchange algorithms (i.e. application logic) at runtime. In our scenario, we want to change the logic used to calculate interest, depending on the type of account we are working with.

Our first step is to define an interface to identify the input and output of our calculations – that is, the account balance and the interest on that balance.

interface InterestCalculationStrategy {

    double calculateInterest(double accountBalance);  //Note the absence of an access modifier - interface methods are implicitly public.

}

Note that our interface is only for account balance – it doesn’t care about account type, since each implementation will already be specific to a particular account type.

The next step is to create strategies to process each of our calculations.

class CurrentAccountInterestCalculation implements InterestCalculationStrategy {

    @Override
    public double calculateInterest(double accountBalance) {
        return accountBalance * (0.02 / 12);
    }
}

class SavingsAccountInterestCalculation implements InterestCalculationStrategy {

    @Override
    public double calculateInterest(double accountBalance) {
        return accountBalance * (0.04 / 12);
    }
}

class MoneyMarketInterestCalculation implements InterestCalculationStrategy {

    @Override
    public double calculateInterest(double accountBalance) {
        return accountBalance * (0.06/12);
    }
}

class HighRollerMoneyMarketInterestCalculation implements InterestCalculationStrategy {

    @Override
    public double calculateInterest(double accountBalance) {
        return accountBalance < 100000.00 ? 0 : accountBalance * (0.075/12)
    }
}

Each calculation is now isolated in its own class, making it much easier to understand individual calculations – they are no longer surrounded by clutter. Next, we’ll refactor our calculator.

public class InterestCalculator {

    //Strategies for calculating interest.
    private final InterestCalculationStrategy currentAccountInterestCalculationStrategy = new CurrentAccountInterestCalculation();
    private final InterestCalculationStrategy savingsAccountInterestCalculationStrategy = new SavingsAccountInterestCalculation();
    private final InterestCalculationStrategy moneyMarketAccountInterestCalculationStrategy = new MoneyMarketInterestCalculation();
    private final InterestCalculationStrategy highRollerMoneyMarketAccountInterestCalculationStrategy = new HighRollerMoneyMarketInterestCalculation();


    public double calculateInterest(AccountTypes accountType, double accountBalance) {
        switch (accountType) {
            case CURRENT: return currentAccountInterestCalculationStrategy.calculateInterest(accountBalance);
            case SAVINGS: return savingsAccountInterestCalculationStrategy.calculateInterest(accountBalance);
            case STANDARD_MONEY_MARKET: return moneyMarketAccountInterestCalculationStrategy.calculateInterest(accountBalance);
            case HIGH_ROLLER_MONEY_MARKET: return highRollerMoneyMarketAccountInterestCalculationStrategy.calculateInterest(accountBalance);
            default:
                return 0;
        }
    }
}

We’ve moved the calculation logic from the calculator itself, but the code still doesn’t look great – it still seems like there is too much going on in a single method. I would even go so far as to say it’s ugly (but I’m known to be pedantic). Fortunately, there is an easy way to clean up this mess – the Factory model.

The Factory model allows us to create objects without necessarily knowing or caring about the type of objects we are creating. This is exactly what our calculator needs – we want calculations, but we don’t care about the details of those calculations. All we really need is a reference to a strategy that knows how to do the appropriate interest calculation for a particular type of account. We can set up our factory as follows:

class InterestCalculationStrategyFactory {

    private final InterestCalculationStrategy currentAccountInterestCalculationStrategy = new CurrentAccountInterestCalculation();
    private final InterestCalculationStrategy savingsAccountInterestCalculationStrategy = new SavingsAccountInterestCalculation();
    private final InterestCalculationStrategy moneyMarketAccountInterestCalculationStrategy = new MoneyMarketInterestCalculation();
    private final InterestCalculationStrategy highRollerMoneyMarketAccountInterestCalculationStrategy = new HighRollerMoneyMarketInterestCalculation();

    //A factory can create a new instance of a class for each request, but since our calculation strategies are stateless,
    //we can hang on to a single instance of each strategy and return that whenever someone asks for it.
    public InterestCalculationStrategy getInterestCalculationStrategy(AccountTypes accountType) {
        switch (accountType) {
            case CURRENT: return currentAccountInterestCalculationStrategy;
            case SAVINGS: return savingsAccountInterestCalculationStrategy;
            case STANDARD_MONEY_MARKET: return moneyMarketAccountInterestCalculationStrategy;
            case HIGH_ROLLER_MONEY_MARKET: return highRollerMoneyMarketAccountInterestCalculationStrategy;
            default: return null;
        }
    }
}

You might think it looks a lot like what we had before. This is the case, but all the logic specific to account types is now encapsulated in a class that satisfies the principle of single responsibility. The Factory is not concerned with the calculations – all it does is match the account types to the appropriate strategies. As a result, we can greatly simplify the code within our calculator class.

public class InterestCalculator {

    private final InterestCalculationStrategyFactory interestCalculationStrategyFactory = new InterestCalculationStrategyFactory();

    public double calculateInterest(AccountTypes accountType, double accountBalance) {
        InterestCalculationStrategy interestCalculationStrategy = interestCalculationStrategyFactory.getInterestCalculationStrategy(accountType);

        if (interestCalculationStrategy != null) {
            return interestCalculationStrategy.calculateInterest(accountBalance);
        } else {
            return 0;
        }
    }
}

It looks a lot better than it used to be, but there is still a part of the code that bothers me – that nasty null check. Let’s do one more refactoring – we’re going to introduce a Null object (also known as Particular case) to handle unexpected types of accounts. It just means that we will have a default policy that will be applied as a last resort. It looks like this.

class NoInterestCalculation implements InterestCalculationStrategy {

    @Override
    public double calculateInterest(double accountBalance) {
        return 0;
    }
}

We can now add No interest Calculation at our factory.

class InterestCalculationStrategyFactory {

    private final InterestCalculationStrategy currentAccountInterestCalculationStrategy = new CurrentAccountInterestCalculation();
    private final InterestCalculationStrategy savingsAccountInterestCalculationStrategy = new SavingsAccountInterestCalculation();
    private final InterestCalculationStrategy moneyMarketAccountInterestCalculationStrategy = new MoneyMarketInterestCalculation();
    private final InterestCalculationStrategy highRollerMoneyMarketAccountInterestCalculationStrategy = new HighRollerMoneyMarketInterestCalculation();
    private final InterestCalculationStrategy noInterestCalculationStrategy = new NoInterestCalculation();

    //A factory can create a new instance of a class for each request, but since our calculation strategies are stateless,
    //we can hang on to a single instance of each strategy and return that whenever someone asks for it.
    public InterestCalculationStrategy getInterestCalculationStrategy(AccountTypes accountType) {
        switch (accountType) {
            case CURRENT: return currentAccountInterestCalculationStrategy;
            case SAVINGS: return savingsAccountInterestCalculationStrategy;
            case STANDARD_MONEY_MARKET: return moneyMarketAccountInterestCalculationStrategy;
            case HIGH_ROLLER_MONEY_MARKET: return highRollerMoneyMarketAccountInterestCalculationStrategy;
            default: return noInterestCalculationStrategy;
        }
    }
}

Now that our factory will no longer return zero values, we can refactor the calculator again. The final version looks like this.

public class InterestCalculator {

    private final InterestCalculationStrategyFactory interestCalculationStrategyFactory = new InterestCalculationStrategyFactory();

    public double calculateInterest(AccountTypes accountType, double accountBalance) {
        InterestCalculationStrategy interestCalculationStrategy = interestCalculationStrategyFactory.getInterestCalculationStrategy(accountType);

        return interestCalculationStrategy.calculateInterest(accountBalance);
    }
}

We’ve effectively removed 75% of the calculator class code, and we won’t have to come back and edit it no matter how many new strategies we decide to add. Nice, clean, simple!

Summary

In this article, we looked at an example of code that got too complex because it had to change its logic depending on the conditions under which it was executed (i.e. different interest calculations for different types accounts). We then extracted the various elements of logic into their own strategies. Despite this, our code was still quite complex, as it knew all the different strategies that could potentially be used. We solved this problem by creating a factory to encapsulate the logic involved in selecting appropriate strategies for various conditions. Finally, we replaced a null check with a null object, which allowed us to simplify our code even further.

As always, feel free to leave a comment if you have any questions / comments / suggestions.


Source link

Previous Movable Design Patterns: Push, Don't Pull (Part 1)
Next Changing the API game: the new top-down design strategy

No Comment

Leave a reply

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