Refactoring to Patterns

In this tutorial, we will use refactoring operations to apply design patterns to improve code maintainability.

To build the project, you need Apache Maven, JDK 11 (or greater) and IntelliJ. IntelliJ IDEA is mandatory to follow the tutorial.

Refactoring is an activity that improves the design of a software source code. During this activity, developers use specific set of source code transformations, named refactoring operations. Refactoring operations are simple code changes that preserve the visible behavior of a program.

During this tutorial—​as well as when refactoring source code—​you must follow two rules:

  1. All Unit Tests must always pass, before and after code changes

  2. The build must always pass and never crash

If you break one of these rules, you should stop refactoring and revert your changes.

1. Getting Ready

Before starting this tutorial, you must fork and clone this repository.
  1. On this project click on Fork to create a fork of the project in your account.

  2. In your fork, go to Project Information  Members, and add as a new member the virtual user called Naobot, with the status "Reporter". This virtual user gives the instructors the right to access your work and will allow us to retrieve your projects.

  3. Clone your project and open it with IntelliJ.

  4. In the project root, add a document named CONTRIBUTORS.md, add your name to it, commit and push your changes.

  5. Do not make your project private, otherwise it will become invisible for the instructors.

2. Introduce Compose Method

In this first exercise, we will simplify a simple method that adds elements to an array of objects.

The "Compose Method"[1] pattern is about producing methods that efficiently communicate what they do and how they do what they do. According to Kent Beck:

«Divide your program into methods that perform one identifiable task. Keep all of the operations in a method at the same level of abstraction. This will naturally result in programs with many small methods, each a few lines long.»

— Kent Beck

A Composed Method consists of calls to well-named methods that are all at the same level of detail. Follow the instructions below to simplify the method add() from class ArrayList:

This class is available here.

Listing 1. The ArrayList Class
package fr.rtp.util;

public class ArrayList {
    private Object[] elements = new Object[10];
    private boolean readOnly;
    private int size = 0;

    public void add(Object child) {
        (1)
        if (!readOnly) {
            int newSize = size + 1;
        (2)
            if (newSize > elements.length) {
                Object[] newElements = new Object[elements.length + 10];
                for (int i = 0; i < size; i++) {
                    newElements[i] = elements[i];
                }
                elements = newElements;
            }
        (3)
            elements[size] = child;
            size++;
        }
    }

    public void setReadOnly(boolean readOnly) {
        this.readOnly = readOnly;
    }

    public boolean isReadOnly() {
        return readOnly;
    }

    public boolean contains(Object child) {
        for (int i = 0; i < size; i++) {
            if (child.equals(elements[i])) return true;
        }
        return false;
    }
}

The ArrayList class is composed of three parts:

1 The read only check part
2 The grow part
3 The actual add behavior part

2.1. Guard Clauses

A guard clause is simply a check (the inverted if) that immediately exits the function, either with a return statement or an exception. Using guard clauses, the possible error cases are identified and there is the respective handling by returning or throwing an adequate exception.

For more information about Guard Clauses, read the refactoring Replace Nested Conditional with Guard Clauses.
  1. Invert the "readonly" check to introduce a "Guard Clause".

  2. Run all tests and check that they still pass after the changes.

    mvn clean test
  3. Commit and push your changes

    git stage ./src/main/java/fr/rtp/util/ArrayList.java
    git commit -m "Replace Nested Conditional with Guard Clause"
    git push

2.2. Extract Private Method

Apply the Extract Method refactoring operation to replace the two lines that actually add an element and create the private method void addElement(Object obj).

  1. Use the Extract Method refactoring from IntelliJ

    1. Select the lines you want to place in a different method.

    2. Go to Refactor  Extract/Introduce  Method…​ and name the new method addElement().

  2. Run all tests and check that they still pass after the changes.

  3. Commit and push your changes

2.3. Replace Magic Numbers

Apply the Extract Constant refactoring operation to replace the magic number 10 and introduce an "Explanatory Variable" named GROWTH_INCREMENT.

Explanatory variables are variables with meaningful names, which held intermediate values of complex calculations to break them up.

The simple use of explanatory variables makes it clear that the first matched group is the key, and the second matched group is the value.

— Kent Beck
  1. Use the Extract Constant refactoring operation from IntelliJ

    1. Select the value (10)

    2. Go to Refactor  Extract/Introduce  Constant…​ and name the new constant GROWTH_INCREMENT.

  2. Run all tests and check that they still pass after the changes.

  3. Commit and push your changes

2.4. Extract Private Method Again

Replace the boolean expression of the if statement by a method call that explains the expression: it checks whether the element array is at its capacity and needs to grow.

  1. Inline variable newSize.

    1. Inside the if condition, click on variable newSize.

    2. Go to Refactor  Inline Variable.

  2. Extract Method

    1. Select the boolean expression inside the if condition

    2. Go to Refactor  Extract/Introduce  Method…​ and name the new method atCapacity().

  3. Run all tests and check that they still pass after the changes.

  4. Commit and push your changes

2.5. Extract the grow() Method

Finally, apply the Extract Method refactoring operation to the part of the code that grows the size of the array, creating the grow() private method.

  1. Extract method

    1. Select the statements inside the if true bloc.

    2. Go to Refactor  Extract/Introduce  Method…​ and name the new method grow().

  2. Run all tests and check that they still pass after the changes.

  3. Commit and push your changes

2.6. Final code

After applying all the previous refactoring operations, your code should look like:

Expected Code
public void add(Object child) {
    if (readOnly) {
        return;
    }
    if (atCapacity()) {
        grow();
    }
    addElement(child);
}

3. Replace Conditional Logic with Strategy

The Strategy Design Pattern defines a family of algorithms, encapsulates each algorithm, and make them interchangeable, letting the algorithm vary independently of the clients that use it.

We can apply this pattern to classes where several methods have similar structure: a sequence of similar conditions.

For instance, let us consider the Loan class[2], from Joshua Kerievsky’s book [1]:

The Loan class is available here.

package fr.rtp.creation.creationmethods;

import java.util.Date;

public class Loan {

    double commitment;
    double outstanding;
    int riskRating;
    Date maturity;
    Date expiry;
    CapitalStrategy capitalStrategy;

    public Loan(double commitment, int riskRating, Date maturity) {
        this(commitment, 0.00, riskRating, maturity, null);
    }

    public Loan(double commitment, int riskRating, Date maturity, Date expiry) {
        this(commitment, 0.00, riskRating, maturity, expiry);
    }

    public Loan(double commitment, double outstanding, int riskRating, Date maturity, Date expiry) {
        this(null, commitment, outstanding, riskRating, maturity, expiry);
    }

    public Loan(CapitalStrategy capitalStrategy, double commitment, int riskRating, Date maturity, Date expiry) {
        this(capitalStrategy, commitment, 0.00, riskRating, maturity, expiry);
    }

    public Loan(CapitalStrategy capitalStrategy, double commitment, double outstanding, int riskRating, Date maturity,
                Date expiry) {
        this.commitment = commitment;
        this.outstanding = outstanding;
        this.riskRating = riskRating;
        this.maturity = maturity;
        this.expiry = expiry;
        this.capitalStrategy = capitalStrategy;

        if (capitalStrategy == null) {
            if (expiry == null)
                this.capitalStrategy = new CapitalStrategyTermLoan();
            else if (maturity == null)
                this.capitalStrategy = new CapitalStrategyRevolver();
            else
                this.capitalStrategy = new CapitalStrategyRCTL();
        }
    }
}

This class deals with calculating capital for three different kinds of bank loans:

Term loan

A loan from a bank for a specific amount that has a specified repayment schedule and a fixed or floating interest rate.

Revolver

A credit that is automatically renewed as debts are paid off.

Advised line

A credit that a financial institution approves and maintains for a customer.

Much of the logic of methods and deals with figuring out whether the loan is a term loan, a revolver, or an advised line.

For example, a null expiry date and a non-null maturity date indicate a term loan. A null maturity and a non-null expiry date indicate a revolver loan.

In this exercise, we will use the Strategy Design Pattern to simplify the calculation of the loan’s capital.

3.1. Create the CapitalStrategy class

  1. Create an empty class named CapitalStrategy.

  2. Add a field named strategy to class Loan.

    Listing 2. The empty `CapitalStrategy`class and its usage
    public class CapitalStrategy {}
    public class Loan {
        private CapitalStrategy strategy = new CapitalStrategy();
        // (...)
    }
  3. Run all tests and check that they still pass after the changes.

  4. Commit and push your changes

3.2. Move Field commitment to CapitalStrategy

We need to move the methods capital() and duration() from class Loan to class CapitalStrategy.

To this end, we will need first to move first the fields and auxiliary methods used by these two methods.

IntelliJ does not implement all known refactoring operations. In some cases, you need to apply refactoring manually.

Be careful: Before changing the source code, think twice!

Ask yourself:

Will this change affect the behavior of the source code?

  1. Select the commitment field at its declaration and go to Refactor  Encapsulate Fields…​.

    1. Do not encapsulate the Set access (the field is never modified)

    2. Set the Accessor Visibility to private.

  2. Move the commitment field to class CapitalStrategy:

    1. Cut&Paste the field declaration to move it. Make it final.

    2. Create a new constructor in class CapitalStrategy that initializes field commitment. Use Code  New…​  Constructor.

    3. Move the initialization of field strategy, from its declaration to the two constructors. At the same time, remove the initializations of field commitment, which no longuer exists.

    4. Copy&Paste the accessor method (getCommitment()).

    5. Change the visibility of the pasted method to protected.

    6. Make the original method delegate its behavior to the new one.

  3. Run all tests and check that they still pass after the changes.

  4. Commit and push your changes

Expected Code
Listing 3. CapitalStrategy class, after changes
public class CapitalStrategy {
    private final double commitment;

    public CapitalStrategy(double commitment) {
        this.commitment = commitment;
    }

    protected double getCommitment() {
        return commitment;
    }
}
Listing 4. Snippet of class Loan, after changes
public class Loan {
    private CapitalStrategy strategy;
    public Loan(double commitment, double outstanding, Date start, Date expiry, Date maturity, int riskRating) {
        strategy = new CapitalStrategy(commitment);
        // (...)
    }
    public Loan(double commitment, Date start, Date maturity, int riskRating) {
        strategy = new CapitalStrategy(commitment);
        // (...)
    }
    // (...)
    private double getCommitment() {
        return strategy.getCommitment();
    }
}

3.3. Move fields maturity, expirity, outstanding, riskRating and start to CapitalStrategy

  1. Encapsulate fields maturity, expirity, outstanding, riskRating and start with a private get accessor.

  2. Cut&Paste the field declarations to move them. Make them final.

  3. Create a new constructor in class CapitalStrategy that initializes field commitment.

    Delete the old constructor and use Code  New…​  Constructor to create a new one.
  4. Move the new accessor methods to CapitalStrategy.

  5. Change the visibility of the pasted method to protected.

  6. Make the original methods delegate their behavior to the new ones.

  7. Run all tests and check that they still pass after the changes.

  8. Commit and push your changes

Expected Code
Listing 5. CapitalStrategy class after changes
public class CapitalStrategy {
    private final double commitment;
    private final double outstanding;
    private final Date maturity;
    private final Date expiry;
    private final Date start;
    private final int riskRating;

    public CapitalStrategy(double commitment, double outstanding, Date maturity, Date expiry,
                           Date start, int riskRating) {
        this.commitment = commitment;
        this.outstanding = outstanding;
        this.maturity = maturity;
        this.expiry = expiry;
        this.start = start;
        this.riskRating = riskRating;
    }

    protected double getCommitment() {
        return commitment;
    }

    protected double getOutstanding() {
        return outstanding;
    }

    protected Date getMaturity() {
        return maturity;
    }

    protected Date getExpiry() {
        return expiry;
    }

    protected Date getStart() {
        return start;
    }

    protected int getRiskRating() {
        return riskRating;
    }
}
Listing 6. Snippet of class Loan, after changes
public class Loan {
    public Loan(double commitment, double outstanding, Date start, Date expiry, Date maturity, int riskRating) {
        strategy = new CapitalStrategy(commitment, outstanding, maturity, expiry, start, riskRating);
        payments = new HashSet<Payment>();
    }

    public Loan(double commitment, Date start, Date maturity, int riskRating) {
        strategy = new CapitalStrategy(commitment, commitment, maturity, null, start, riskRating);
        payments = new HashSet<Payment>();
    }
// (...)
    private double getOutstanding() {
        return strategy.getOutstanding();
    }

    private Date getMaturity() {
        return strategy.getMaturity();
    }

    private Date getExpiry() {
        return strategy.getExpiry();
    }

    private Date getStart() {
        return strategy.getStart();
    }

    public int getRiskRating() {
        return strategy.getRiskRating();
    }
}

3.4. Move method yearsTo() to CapitalStrategy

  1. Move, using Cut&Paste, fields today, MILLIS_PER_DAY and DAYS_PER_YEAR to class CapitalStrategy

  2. Copy, using Copy&Paste, method yearsTo() to class CapitalStrategy.

    1. Change the visibility of the new method to protected.

  3. Replace the body of the original method with a delegation to the new method.

  4. Run all tests and check that they still pass after the changes.

  5. Commit and push your changes

Expected Code
Listing 7. Method Loan::yearsTo() after changes
private double yearsTo(Date endDate) {
    return strategy.yearsTo(endDate);
}
Listing 8. Method CapitalStrategy::yearsTo() after changes
protected double yearsTo(Date endDate) {
    Date beginDate = (today == null ? getStart() : today);
    return ((endDate.getTime() - beginDate.getTime()) / MILLIS_PER_DAY) / DAYS_PER_YEAR;
}

3.5. Move method duration() to CapitalStrategy

Here we will use a different approach to move a method from one class to another, without using Copy&Paste
  1. Change the visibility of method weightedAverageDuration() to protected.

  2. Select all the lines of the body of method duration().

  3. Go to Refactor  Extract/Introduce  Method…​ and extract the new method.

    1. Use a temporary name for the method, extracted() is just fine.

  4. Go to Refactor  Move Instance Method to move the new method to class CapitalStrategy.

    1. Do not worry about the visibility warnings.

  5. Replace loan by this in the new method, when the method already exists in class CapitalStrategy

    Replace by

    loan.getExpiry()

    this.getExpiry()

    loan.getMaturity()

    this.getMaturity()

    loan.yearsTo(loan.getExpiry())

    this.yearsTo(this.getExpiry())

  6. Select the new method name, go to Refactor  Rename…​ and rename it to duration().

  7. Run all tests and check that they still pass after the changes.

  8. Commit and push your changes

Expected Code
Listing 9. Method Loan::duration() after changes

3.6. Move method capital() to CapitalStrategy

First step
  1. Move method unusedRiskAmount()

    1. This method has two method calls: getCommitment() and getOutstanding().

    2. Inline these calls:

      1. Click on the first method call then go to Refactor  Inline Method…​

      2. Choose "Inline this only and keep the method"

      3. Do the same with the second method call.

Expected Code after 1st step
Listing 10. Method unusedRiskAmount() after inlines
private double unusedRiskAmount() {
    return (strategy.getCommitment() - strategy.getOutstanding());
}
Second step
  1. Continue moving method unusedRiskAmount()

    1. Click on the name of method outstandingRiskAmount().

    2. Go to Refactor  Move Instance Method…​ and move method to class CapitalStrategy.

    3. Choose visibility "Escalate" and let IntelliJ choose the appropriate visibility.

  2. Move method outstandingRiskAmount()

    1. Inline the only method call

    2. Move method to class CapitalStrategy.

  3. Move method riskFactor()

    1. Inline the method call getRiskRating()

    2. Move method to class CapitalStrategy.

  4. Move method unusedRiskFactor()

    1. Inline the method call getRiskRating()

    2. Move method to class CapitalStrategy.

  5. Move method getUnusedPercentage()

    1. Go to Refactor  Move Instance Method…​ and move method to Capital Strategy

  6. Finally, move method capital()

    1. Inline all method calls

Expected Code after 2nd step
Listing 11. Method capital() after inlines and before move
public double capital() {
    if (strategy.getExpiry() == null && strategy.getMaturity() != null) // Term Loan
        return strategy.getCommitment() * strategy.duration(this) * strategy.riskFactor();
    if (strategy.getExpiry() != null && strategy.getMaturity() == null) {
        if (strategy.getUnusedPercentage() != 1.0) // Revolver
            return strategy.getCommitment() * strategy.getUnusedPercentage() * strategy.duration(this) * strategy.riskFactor();
        else // Advised Line
return (strategy.outstandingRiskAmount() * strategy.duration(this) * strategy.riskFactor())
+ (this.strategy.unusedRiskAmount() * strategy.duration(this) * strategy.unusedRiskFactor());
    }
    return 0.0;
}
Third step
  1. Select the method body (all method code lines) and go to Refactor  Extract/Introduce  Method

    1. Choose a temporary name for the method, for instance extractedCapital()

  2. Click on the name of the new method and go to Refactor  Move Instance Method…​ and move it to class CapitalStrategy

  3. Rename the new method to capital()

In the end, you should have the following code:

Expected Code after 3rd step
Listing 12. Method Loan::capital() after changes
public double capital() {
    return strategy.capital(this);
}
Listing 13. Method CapitalStrategy::capital() after changes
double capital(Loan loan) {
    if (getExpiry() == null && getMaturity() != null) // Term Loan
        return getCommitment() * duration(loan) * riskFactor();
    if (getExpiry() != null && getMaturity() == null) {
        if (getUnusedPercentage() != 1.0) // Revolver
            return getCommitment() * getUnusedPercentage() * duration(loan) * riskFactor();
        else // Advised Line
            return (outstandingRiskAmount() * duration(loan) * riskFactor())
                    + (unusedRiskAmount() * duration(loan) * unusedRiskFactor());
    }
    return 0.0;
}
Last steps
  1. Run all tests and check that they still pass after the changes.

  2. Commit and push your changes

3.7. Replace Conditional with Polymorphism

Now that we migrated methods capital() and duration() to CapitalStrategy, we will create the classes that play the Concrete Strategy roles and move that methods to these classes.

  1. Create the Concrete Strategy classes

    1. Create the classes RevolverStrategy, TermLoanStrategy, and AdvisedLineStrategy.

    2. Make them subclasses of CapitalStrategy.

    3. Go to Code  Generate…​  Constructor to add a constructor to these classes.

  2. Instantiate the correct strategy for a Loan.

    1. Modify the constructors of class Loan and make them instantiate the correct strategy.

According to the code:

  • When expiry is null, the strategy should be "Term Loan".

  • When expiry is not null and maturity is null, the strategy should be "Revolver".

  • Otherwise, the strategy should be "Advised Line".

  1. Make classe CapitalStrategy abstract.

  2. Push down method capital()

    1. Click on the method name, then go to Refactor  Push Members Down…​

    2. Add an abstract method capital() to class CapitalStrategy.

  3. Open class RevolverStrategy and remove the parts of code that do not concern a revolver loan

Expected Code
Listing 14. Method RevolverStrategy::capital() after changes
double capital(Loan loan) {
    return getCommitment() * getUnusedPercentage() * duration(loan) * riskFactor();
}
  1. Open class TermLoanStrategy and remove the parts of code that do not concern a revolver loan

Expected Code
Listing 15. Method TermLoanStrategy::capital() after changes
double capital(Loan loan) {
    return getCommitment() * duration(loan) * riskFactor();
}
  1. Open class AdvisedLineStrategy and remove the parts of code that do not concern a revolver loan

Expected Code
Listing 16. Method AdvisedLineStrategy::capital() after changes
double capital(Loan loan) {
    return (outstandingRiskAmount() * duration(loan) * riskFactor())
+ (unusedRiskAmount() * duration(loan) * unusedRiskFactor());
}
  1. Repeat the same operations for method CapitalStrategy::duration()

  2. Run all tests and check that they still pass after the changes.

  3. Commit and push your changes

4. Chain Constructors

Classes may have several constructors: this is normal, as there may be different ways to instantiate objects of a same class. However, maintenance problems arise when code snippets are duplicated across the code source.

Consider the three constructors of class BankLoan presented in listing Listing 17, which have duplicated code. We will use the refactoring operation named Chain Constructors, whose goal is to remove duplication in constructors by making them call each other.

First, we analyze these constructors to find out which one is the catch-all constructor, the one that handles all the construction details. It seems that it should be constructor 3, since making constructors 1 and 2 call 3 can be achieved with a minimum amount of work.

Listing 17. Class BankLoan
package fr.rtp.utilities.chainconstructors;

import java.util.Date;

public class BankLoan {

    BankCapitalStrategy strategy;
    float national;
    float outstanding;
    int rating;
    Date expiry;
    Date maturity;

    public BankLoan(float national, float outstanding, int rating, Date expiry) {
        this.strategy = new TermROC();
        this.national = national;
        this.outstanding = outstanding;
        this.rating = rating;
        this.expiry = expiry;
    }

    public BankLoan(float national, float outstanding, int rating, Date expiry, Date maturity) {
        this.strategy = new RevolvingTermROC();
        this.national = national;
        this.outstanding = outstanding;
        this.rating = rating;
        this.expiry = expiry;
        this.maturity = maturity;
    }

    public BankLoan(BankCapitalStrategy strategy, float national, float outstanding, int rating, Date expiry, Date maturity) {
        this.strategy = strategy;
        this.national = national;
        this.outstanding = outstanding;
        this.rating = rating;
        this.expiry = expiry;
        this.maturity = maturity;
    }
}
  1. Change constructor 1 to make it call constructor 3.

  2. Change constructor 2 to make it also call constructor 3.

  3. Run all tests and check that they still pass after the changes.

  4. Commit and push your changes

Expected Code
Listing 18. BankLoan class after changes
public class BankLoan {
    // (...)
    public BankLoan(float national, float outstanding, int rating, Date expiry) {
        this(new TermROC(), national, outstanding, rating, expiry, null);
    }

    public BankLoan(float national, float outstanding, int rating, Date expiry, Date maturity) {
        this(new RevolvingTermROC(), national, outstanding, rating, expiry, maturity);
    }

    public BankLoan(BankCapitalStrategy strategy, float national, float outstanding, int rating, Date expiry, Date maturity) {
        this.strategy = strategy;
        this.national = national;
        this.outstanding = outstanding;
        this.rating = rating;
        this.expiry = expiry;
        this.maturity = maturity;
    }
}

5. Replace Constructors with Creation Methods

The goal of the Replace Constructors with Creation Methods refactoring operation is to replace constructors with intention-revealing creation methods that return object instances.

Creation Method is a generic term to designate any method that creates instances of a class. For instance, the Factory Method and the Builder Design Patterns use Creation Methods.

Creation methods have at least two advantages, that cannot be achieved in Java. First, they can have different names and thus communicate intention efficiently. Second, different creation methods can have the same number of parameters.

We will apply this refactoring to improve the constructions of class Loan. Consider the source code of this class, presented below.

package fr.rtp.creation.creationmethods;

import java.util.Date;

public class Loan {

    double commitment;
    double outstanding;
    int riskRating;
    Date maturity;
    Date expiry;
    CapitalStrategy capitalStrategy;

    public Loan(double commitment, int riskRating, Date maturity) {
        this(commitment, 0.00, riskRating, maturity, null);
    }

    public Loan(double commitment, int riskRating, Date maturity, Date expiry) {
        this(commitment, 0.00, riskRating, maturity, expiry);
    }

    public Loan(double commitment, double outstanding, int riskRating, Date maturity, Date expiry) {
        this(null, commitment, outstanding, riskRating, maturity, expiry);
    }

    public Loan(CapitalStrategy capitalStrategy, double commitment, int riskRating, Date maturity, Date expiry) {
        this(capitalStrategy, commitment, 0.00, riskRating, maturity, expiry);
    }

    public Loan(CapitalStrategy capitalStrategy, double commitment, double outstanding, int riskRating, Date maturity,
                Date expiry) {
        this.commitment = commitment;
        this.outstanding = outstanding;
        this.riskRating = riskRating;
        this.maturity = maturity;
        this.expiry = expiry;
        this.capitalStrategy = capitalStrategy;

        if (capitalStrategy == null) {
            if (expiry == null)
                this.capitalStrategy = new CapitalStrategyTermLoan();
            else if (maturity == null)
                this.capitalStrategy = new CapitalStrategyRevolver();
            else
                this.capitalStrategy = new CapitalStrategyRCTL();
        }
    }
}
  1. To apply this refactoring, we need to find a code that calls one of these constructors. For instance, in a test case, such as CapitalCalculationTest

  2. Create Creation Method

    1. Select the code that creates an instance of Loan, i.e. new Loan(commitment, riskRating, maturity)

    2. Go to Refactor  Extract/Introduce  Method…​. Name the method createTermLoan()

  3. Make it static and public

    1. Click on the method name

    2. Go to Refactor  Make static…​

    3. Make it public

      Listing 19. Creation Method createTermLoan()
      public static Loan createTermLoan(Date maturity, int riskRating, double commitment) {
          return new Loan(commitment, riskRating, maturity);
      }
  4. Move method to class Loan

    1. Click on the method name and go to Refactor  Move Members…​

    2. Write down the destination (to) class: fr.rtp.creation.creationmethods.Loan.

  5. After doing that, we will need to find all callers of the constructor and update them to call createTermLoan().

    1. Click on the first constructor and then go to Navigate  Declaration or Usages.

    2. If you find any usage other than the creation method, make it call createTermLoan().

  6. Inline method

    1. Since now the method is now the only caller on the constructor, we can apply the Inline Method refactoring to this constructor.

    2. Inside the creation method, click on the construction call and then go to Refactor  Inline Method.. choose Inline all and remove the method.

  7. Repeat the same procedure to the other constructors, to create additional creation methods on class Loan.

    Listing 20. New Creation Methods
    public static Loan newRevolver(double commitment, Date start, Date expiry, int riskRating) {
        return new Loan(commitment, 0, start, expiry, null, riskRating, new CapitalStrategyRevolver());
    }
    
    public static Loan newAdvisedLine(double commitment, Date start, Date expiry, int riskRating) {
        if (riskRating > 3) return null;
        Loan advisedLine = new Loan(commitment, 0, start, expiry, null, riskRating, new CapitalStrategyAdvisedLine()); advisedLine.setUnusedPercentage(0.1);
        return advisedLine;
    }
  8. Last step, since the constructors are only used by creation methods, they can become private.

  9. Run all tests and check that they still pass after the changes.

  10. Commit and push your changes

6. Replace State-Altering Conditionals with State

The State Design Pattern applies to classes whose objet behaviors are strongly dependent on their internal state.

The State Design Pattern encapsulates the behavior specific to each state and replaces conditional checks by delegations.

For instance, let us consider the SystemPermission class, from Joshua Kerievsky’s book [1]:

package fr.rtp.simplification.condwithstate;

public class SystemPermission {

  private SystemProfile profile;
  private SystemUser requestor;
  private SystemAdmin admin;
  private boolean isGranted;
  private String state;

  public final static String REQUESTED = "REQUESTED";
  public final static String CLAIMED = "CLAIMED";
  public final static String GRANTED = "GRANTED";
  public final static String DENIED = "DENIED";

  public SystemPermission(SystemUser requestor, SystemProfile profile) {
    this.requestor = requestor;
    this.profile = profile;
    state = REQUESTED;
    isGranted = false;
    notifyAdminOfPermissionRequest();
  }

  public void claimedBy(SystemAdmin admin) {
    if (!state.equals(REQUESTED)) {
      return;
    }
    willBeHandledBy(admin);
    state = CLAIMED;
  }

  public void deniedBy(SystemAdmin admin) {
    if (!state.equals(CLAIMED)) {
      return;
    }
    if (!admin.equals(this.admin)) {
      return;
    }
    isGranted = false;
    state = DENIED;
    notifyUserOfPermissionRequestResult();
  }

  public void grantedBy(SystemAdmin admin) {
    if (!state.equals(CLAIMED)) {
      return;
    }
    if (!admin.equals(this.admin)) {
      return;
    }
    state = GRANTED;
    isGranted = true;
    notifyUserOfPermissionRequestResult();
  }

  private void willBeHandledBy(SystemAdmin admin) {
    this.admin = admin;
  }

  private void notifyUserOfPermissionRequestResult() {
  }

  private void notifyAdminOfPermissionRequest() {
  }

  public String state() {
    return state;
  }

  public boolean isGranted() {
    return isGranted;
  }

}

The SystemPermission plays the role of Context in the State Design Pattern. It has a state field, i.e. a field that gets assigned to or compared against a family of constants during state transitions. In this class, the state field is also named state.

6.1. Replace Type Code with Class

At first, we will apply the refactoring operation named Replace Type Code with Class.

We will change the type of the original state field, from String to a new class named PermissionState.

First step
  1. Create a new class named PermissionState inside package fr.rtp.simplification.condwithstate.

  2. Copy the state field to PermissionState and make it private and final. A value will be set for this field only from the constructor.

  3. Create a getter for the field.

Expected code after first step
Listing 21. Class PermissionState, initial version
public class PermissionState {
    private final String state;

    public PermissionState(String state) {
        this.state = state;
    }

    public String getState() {
        return state;
    }
}
Second step
  1. Move state constants to class PermissionState.

    1. Click on constant REQUESTED, then go to Refactor  Move Members…​, and move the constant to class fr.rtp.simplification.condwithstate.PermissionState.

    2. Change the types and the initialization values of the constants REQUESTED, CLAIMED, GRANTED, and DENIED. Each field initialization creates an instance of PermissionState corresponding to this value of state.

Expected code after second step
Listing 22. Constant initialization
public final static PermissionState REQUESTED = new PermissionState("REQUESTED");
Third step
  1. Add methods equals() and hashCode() to class PermissionState.

    1. Go to Code  Generate…​ and choose "equals() and hashCode()".

  2. In class SystemPermission:

    1. Replace the type of field state with PermissionState.

    2. Change the getter of the state field, so that it calls the PermissionState class getter.

  3. Run all tests and check that they still pass after the changes.

  4. Commit and push your changes.

6.2. Extract subclasses

Now, we will apply the refactoring operation named Extract Subclass to class PermissionState.

  1. Create a class, subclass of PermissionState for each constant: Requested, Claimed, etc.

    1. After creating each subclass, go to Code  Generate…​ and choose "Constructor" to create a constructor.

  2. Update the constant initialization in class PermissionState, so that each refers to the correct subclass instance of the state superclass.

    Listing 23. Constant initialization, updated
    public final static PermissionState REQUESTED = new Requested("REQUESTED");
  3. Now, declare the state superclass to be abstract.

  4. Run all tests and check that they still pass after the changes.

  5. Commit and push your changes.

6.3. Move state-dependent methods

Several methods from class SystemPermission implement a behavior that highly depends on the current state: grantedBy(), claimedBy(), and deniedBy().

We will move these methods to the state classes and replace the original methods by a delegation.

  1. Before moving the methods, we need to prepare the body of these methods, making the used fields and called methods accessible from the state classes.

    1. Change the visibility of methods willBeHandledBy(), notifyUserOfPermissionRequestResult(), and notifyAdminOfPermissionRequest() to package-local or protected.

    2. Encapsulate field state. Click on the field declaration, then go to Refactor  Encapsulate fields…​. Set the visibility of accessors to package-local.

    3. Do the same with field isGranted and admin.

  2. Move method claimedBy() to class PermissionState.

    1. Select the body of the method, then go to Refactor  Extract/Introduce  Method…​.

    2. Use a temporary name for the new method, for instance, extracted() or extractedClaimedBy().

    3. Click on the name of the extracted method and go to Refactor  Move Instance Method…​ and choose PermissionState.

    4. After moving the method rename the method to claimedBy().

  3. Now, repeat the previsous steps for methods deniedBy() and grantedBy().

  4. Run all tests and check that they still pass after the changes.

  5. Commit and push your changes.

6.4. Replace Conditional with Polymorphism

Now, we will remove the state-related conditionals from the moved methods and place in each state subclass the part of method body related to it.

  1. In class PermissionState:

    1. Push-down method claimedBy()

    2. Create an abstract method with the same signature in class PermissionState to avoid accessibility errors.

    3. Now, analyze all the pushed methods and remove the code that will never be executed.

    4. For instance, the behavior of method claimedBy() is only executed when the state is Requested. Nothing is executed in other states.

Expected code
Listing 24. Method Requested::claimedBy(), after changes
void claimedBy(SystemAdmin admin, SystemPermission systemPermission) {
  systemPermission.willBeHandledBy(admin);
  systemPermission.setState(CLAIMED);
}
Listing 25. Method Claimed::claimedBy(), after changes
void claimedBy(SystemAdmin admin, SystemPermission systemPermission) {
}
  1. Repeat the previous steps for methods deniedBy() and grantedBy().

  2. Run all tests and check that they still pass after the changes.

  3. Commit and push your changes.

References

  • [1] Andy Hunt & Dave Thomas. The Pragmatic Programmer: From Journeyman to Master. Addison-Wesley. 1999.