DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Security by Design: Building Full-Stack Applications With DevSecOps
  • How Large Tech Companies Architect Resilient Systems for Millions of Users
  • Building Security into the Feature During the Design Phase
  • Build a Scalable E-commerce Platform: System Design Overview

Trending

  • Designing for Sustainability: The Rise of Green Software
  • Mastering Advanced Traffic Management in Multi-Cloud Kubernetes: Scaling With Multiple Istio Ingress Gateways
  • Power BI Embedded Analytics — Part 3: Power BI Embedded Demo
  • Assessing Bias in AI Chatbot Responses

Design Patterns: The Strategy and Factory Patterns

The Strategy pattern allows us to dynamically swap out algorithms at runtime, and the Factory pattern allows us to create objects as needed. See the synergy between them.

By 
Riaan Nel user avatar
Riaan Nel
·
Dec. 19, 16 · Tutorial
Likes (62)
Comment
Save
Tweet
Share
85.7K Views

Join the DZone community and get the full member experience.

Join For Free

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 behavioral pattern.

The Problem

As before, we'll pretend that we're on a team of Java developers working for a bank. This time around, 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, rumor 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 swap 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.

Factory (object-oriented programming) Design

Published at DZone with permission of Riaan Nel. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Security by Design: Building Full-Stack Applications With DevSecOps
  • How Large Tech Companies Architect Resilient Systems for Millions of Users
  • Building Security into the Feature During the Design Phase
  • Build a Scalable E-commerce Platform: System Design Overview

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!