Design Patterns: The Strategy and Factory Patterns
Courtesy of https://pixabay.com/en/industrial-decadence-old-factory-1765346/

Design Patterns: The Strategy and Factory Patterns

This is the second article in my series on design patterns. In the first one, we had a look at the Builder pattern. We also briefly discussed the benefits of patterns. If you haven't read it yet, it might be a good idea to check out the first two paragraphs before you continue with this article.

When I sat down to start planning my next post, I was really torn between the Strategy and Factory patterns. I've used both of them to great effect in my code in the past and I think both of them are fundamental patterns that belong in every OO developer's vocabulary. As it turns out, the Factory pattern complements the Strategy pattern rather nicely, so I decided to cover both in a single post. As was the case with the Builder pattern that we looked at last time, the Factory pattern is a creational pattern. The Strategy pattern, on the other hand, is a behavioural pattern.

The Problem

As before, we'll pretend that we're on a team of Java developers working for a bank. This time round, we're calculating monthly interest on different types of bank accounts. Initially, we're only dealing with two account types - current accounts paying 2% interest per annum, and savings accounts paying 4% per annum. Interest will not be applicable to any other types of accounts. Our account types are defined by an enum.

enum AccountTypes {CURRENT, SAVINGS}

Based on these account types, 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 of 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 evident that our code gets messier with every set of new requirements that we implement. We have all these business rules bundled into one class which is becoming harder and harder to understand. Also, rumour has it that the asset financing department of the bank has heard of our new interest calculator, and they would like to use it to calculate interest on loans to customers. However, their interest rates aren't fixed - they are linked to interest rates from a central bank, which we'll have to retrieve via a web service. Not only are we starting to deal with more account types, but the calculation logic is also growing in complexity.

If we keep on adding more and more business rules into our calculator, we're going to end up with something that could become very difficult to maintain. Sure, we can try and extract each calculation into its own method, which might be slightly cleaner, but ultimately, that will still be lipstick on a pig.

The problem that we have is this:

  • We have a single, convoluted, hard-to-maintain method that is trying to deal with a number of different scenarios.

The Strategy pattern can help us to address this issue.

The Pattern(s)

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

Our first step is to define an interface to identify the input and output of our calculations - i.e. 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 concerned with the account balance - it doesn't care about the account type, since each implementation will already be specific to a particular account type.

The next step is to create strategies to deal with 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 to its own class, making it much easier to understand individual calculations - they're not surrounded by clutter anymore. 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 out of the calculator itself, but the code still doesn't look great - it still seems like there are too many things happening in one method. I would even go so far as to call it ugly (but I'm known to be pedantic). Fortunately, there is an easy way to clean up this mess - the Factory pattern.

The Factory pattern allows us to create objects without necessarily knowing or caring about the type of objects that 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 create 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 that this looks very similar to what we had before. It does, but all of the logic specific to account types is now encapsulated in one class that satisfies the single responsibility principle. The factory isn't concerned with calculations - all it does is to match 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;
        }
    }
}

This looks much better than before, but there's still one part of the code that bugs me - that nasty null check. Let's do one more refactoring - we'll introduce a Null Object (also known as a Special Case) to deal with unexpected account types. This simply means that we'll have a default strategy that will be applied as a last resort. It looks as follows.

class NoInterestCalculation implements InterestCalculationStrategy {

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

We can now add NoInterestCalculation to 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 nulls, we can refactor the calculator once 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 code within the calculator class, and we won't have to come back and change it, regardless of how many new strategies we decide to add. Nice, clean, simple!

Summary

In this article, we looked at an example of code that became overly complex as a result of the fact that it had to alter its logic based on the conditions under which it was executed (i.e. different interest calculations for different account types). We then extracted the various bits of logic into strategies of their own. Despite this, our code was still fairly complex, since it had knowledge of all of the different strategies that could potentially be used. We addressed this by creating a factory to encapsulate the logic concerned with 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 drop a comment if you have any questions/comments/suggestions.

Additional Reading

https://en.wikipedia.org/wiki/Factory_method_pattern

?https://en.wikipedia.org/wiki/Strategy_pattern

Musa Dabra

Software Engineer | ML Enthusiast | Develop scalable and secured microservices for enterprise application.

2 å¹´

Thank you for this Riaan Nel I was looking for a similar implementation, while searching google I came across this, in my case I had to Use Abstract factory and strategy. This also helps as it’s more of real world scenario.

Datta Desai

Senior Software Engineer

5 å¹´

Hi Riaan, Thanks a lot for a article. There are three types of factory design pattern namely 1. Simple Factory Pattern(also called Static Factory Pattern) 2. Factory Method Pattern 3. Abstract Factory Pattern In above article, example is related to "Simple Factory Pattern". Could you please throw some light on "Factory Method Pattern" and "Abstract Factory Pattern".

要查看或添加评论,请登录

Riaan Nel的更多文章

  • The CTO’s Guide: Welcome to a World of Responsibilities

    The CTO’s Guide: Welcome to a World of Responsibilities

    When you transition into a CTO role for the first time, your world changes. You have a bunch of new responsibilities…

    2 条评论
  • On Architects, Architecture, and Failures

    On Architects, Architecture, and Failures

    Let’s consider two things: 1.) Bad things happen to good people 2.

    4 条评论
  • Mentorship 101: A Guide to Growth

    Mentorship 101: A Guide to Growth

    I recently presented a talk on mentorship at the Dariel Speaker's Forge. This article covers most of what I spoke about.

    8 条评论
  • Starting your career as a developer? Here's some advice

    Starting your career as a developer? Here's some advice

    I recently had a conversation with one of my colleagues. This post was inspired by that conversation.

    11 条评论
  • Programming: An Exercise In Frustration (and why that's okay)

    Programming: An Exercise In Frustration (and why that's okay)

    “It is common sense to take a method and try it. If it fails, admit it frankly and try another.

    2 条评论
  • Mob Programming Memoirs: Agile Collaboration On Steroids!

    Mob Programming Memoirs: Agile Collaboration On Steroids!

    This article was originally published on the OfferZen blog. "If you want to go quickly, go alone.

    2 条评论
  • On Collaboration and Innovation

    On Collaboration and Innovation

    I recently spent some time at a corporate hackathon. The event brought together people from various backgrounds –…

  • What makes (dev) teams great?

    What makes (dev) teams great?

    "Great things in business are never done by one person. They're done by a team of people.

    7 条评论
  • Design Patterns: The Builder Pattern

    Design Patterns: The Builder Pattern

    I've been meaning to write a series of articles on design patterns for quite a while. Patterns are incredibly valuable…

    11 条评论
  • Programmers - what on earth do they actually do!?

    Programmers - what on earth do they actually do!?

    I am a programmer. Professionally, I've been doing it for somewhere between eight and nine years, but I started coding…

    1 条评论

社区洞察

其他会员也浏览了