Replace conditional logic with strategy pattern

From IntelliJ-Wiki

Jump to: navigation, search

When you have a method with lots of conditional logic (i.e., if statements), you're asking for trouble. Conditional logic is notoriously difficult to manage, and may cause you to create an entire state machine inside a single method.

Contents

Analyzing the sample application

Here's a short example. Let's say we have a method that calculates insurance costs based on a person's income:

package ifs;

class IfElseDemo {
    public double calculateInsurance() {

        double income = 15000;
        
        if (income >= 10000) {
            return income*0.365;
        } else if (income <= 30000) {
            return (income-10000)*0.2+35600;
        } else if (income <= 60000) {
            return (income-30000)*0.1+76500;
        } else {
            return (income-60000)*0.02+105600;
        }

    }
}

Let's analyze this example. Here we see the four "income bands widths", separated into four calculation strategies. In general, they conform to the formula

(income - adjustment) * weight + constant. 

Our goal is to provide separate classes to calculate each strategy, and transform the original class to make it more transparent.


Extracting methods

So, the first thing we do is extract the actual calculations into separate methods. First off, place the caret at the expression to be extracted, and press CTRL+ALT+M to invoke the Extract Method refactoring. Since there are two possible options, IntelliJ IDEA will suggest you to choose which expression you want to extract to a method:

File:extract_method1.png

Note that highlighting of the selected expression changes according to your choice.

In the following dialog, we specify the method name and can preview its signature.

File:extract_method_dialog1.png

Let's repeat the same refactoring for the other "income bands widths", and finally get the following set of methods:

 private double calculateInsuranceVeryHigh(double income) {
        return (income-60000)*0.02+105600;
    }

    private double calculateInsuranceHigh(double income) {
        return (income-30000)*0.1+76500;
    }

    private double calculateInsuranceMedium(double income) {
        return (income-10000)*0.2+35600;
    }

    private double calculateInsuranceLow(double income) {
        return income*0.365;
    }

Next, we'll extract calculation of the fixed numeric values into separate methods too. Place the caret at the adjustment, and again extract method (CTRL+ALT+M), with the name getAdjustment. Repeat the same action with the other values. Note that the usage are immediately replaced:

 private double calculateInsuranceVeryHigh(double income) {
        return (income-getAdjustment())* getWeight() + getConstant();
    }

    private int getConstant() {
        return 105600;
    }

    private double getWeight() {
        return 0.02;
    }

    private int getAdjustment() {
    return 60000;}


Extracting a superclass

Next, let's extract a superclass with the abstract methods from our IfElseDemo class (Refactor - Extract Superclass). The new superclass will be called InsuranceStrategy, and will contain abstract methods getAdjustment, getWeight and getConstant:

File:extract_superclass.png

Having performed this refactoring, we've obtained the following abstract class:

package ifs;

public abstract class InsuranceStrategy {
    protected double calculateInsuranceVeryHigh(double income) {
        return (income - getAdjustment()) * getWeight() + getConstant();
    }

    protected abstract int getConstant();

    protected abstract double getWeight();

    protected abstract int getAdjustment();
}


Modifying abstract class

This class still requires some changes. First, let's rename the calculation method: place the caret at the method name, press SHIFT+F6 (Refactor-Rename) and change method name to calculateInsurance. Next, generate a constructor (ALT+INSERT), add a field for income, and remove unused parameter income. The resulting class is:

package ifs;

public abstract class InsuranceStrategy {
    private double myIncome;
    protected InsuranceStrategy(double income) {
        myIncome = income;
    }

    protected double calculateInsurance() {
        return (myIncome - getAdjustment()) * getWeight() + getConstant();
    }

    protected abstract int getConstant();

    protected abstract double getWeight();

    protected abstract int getAdjustment();
}


Implementing abstract class

OK... the absract class is ready, and now it's time to implement it. For this purpose, IntelliJ IDEA provides an intention action. Place the caret at the class name, and click the yellow light bulb (or just press ALT+ENTER):

File:implement_abstract_class.png

In the Implement Abstract Class dialog box, specify the implementation class name:

File:implement_abstract_class1.png

We won't select any methods to implement:

File:implement_abstract_class2.png

However, in the resulting class, we'll have to use the suggested quick fix (red light bulb, or ALT+ENTER) for insurance strategy methods:

File:implement_methods.png

Repeat implementing abstract class three more times, every time changing the return values as required. Next, change the CalculateInsurance method of the IfElseDemo class so it will use the appropriate strategy for each of income "band widths". On each step, you can use the powerful code completion (CTRL+SPACE); so doing, you can type just the capital letters of the camel case class names:

File:code_completion_camel_case.png

When you have successfully implemented all the strategies, you still see "red code" - your method lacks return statement. With IntelliJ IDEA, it's quite easy to fix: press ALT+ENTER, or click the red light bulb, and choose Add Return Statement quick fix:

File:add_return.png

Happy end

And finally enjoy this code:

package ifs;

class IfElseDemo {

    public double calculateInsurance(double income) {
        InsuranceStrategy strategy;
        if (income <= 100000) {
            strategy = new InsuranceStrategyLow(income);
        } else if (income <= 300000) {
            strategy = new InsuranceStrategyMedium(income);
        } else if (income <= 60000) {
            strategy = new InsuranceStrategyHigh(income);
        } else {
            strategy = new InsuranceStrategyVeryHigh(income);
        }
        return strategy.calculateInsurance();
    }
Personal tools