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:
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. |
-
On this project click on Fork to create a fork of the project in your account.
-
In your fork, go to , 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.
-
Clone your project and open it with IntelliJ.
-
In the project root, add a document named
CONTRIBUTORS.md, add your name to it, commit and push your changes. -
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.»
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.
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 areturnstatement 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. |
|
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).
|
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.
|
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.
|
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.
|
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.
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
|
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:
|
|
Expected Code
public class CapitalStrategy {
private final double commitment;
public CapitalStrategy(double commitment) {
this.commitment = commitment;
}
protected double getCommitment() {
return commitment;
}
}
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
|
Expected Code
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;
}
}
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
|
Expected Code
private double yearsTo(Date endDate) {
return strategy.yearsTo(endDate);
}
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 |
|
Expected Code
Loan::duration() after changes
3.6. Move method capital() to CapitalStrategy
|
First step
|
Expected Code after 1st step
unusedRiskAmount() after inlinesprivate double unusedRiskAmount() {
return (strategy.getCommitment() - strategy.getOutstanding());
}
|
Second step
|
Expected Code after 2nd step
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
|
In the end, you should have the following code:
Expected Code after 3rd step
public double capital() {
return strategy.capital(this);
}
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
|
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.
|
|
According to the code:
|
|
Expected Code
double capital(Loan loan) {
return getCommitment() * getUnusedPercentage() * duration(loan) * riskFactor();
}
|
Expected Code
double capital(Loan loan) {
return getCommitment() * duration(loan) * riskFactor();
}
|
Expected Code
double capital(Loan loan) {
return (outstandingRiskAmount() * duration(loan) * riskFactor())
+ (unusedRiskAmount() * duration(loan) * unusedRiskFactor());
}
|
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.
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;
}
}
|
Expected Code
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();
}
}
}
|
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
|
Expected code after first step
public class PermissionState {
private final String state;
public PermissionState(String state) {
this.state = state;
}
public String getState() {
return state;
}
}
|
Second step
|
Expected code after second step
public final static PermissionState REQUESTED = new PermissionState("REQUESTED");
|
Third step
|
6.2. Extract subclasses
Now, we will apply the refactoring operation named
Extract Subclass to class PermissionState.
|
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.
|
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.
|
Expected code
void claimedBy(SystemAdmin admin, SystemPermission systemPermission) {
systemPermission.willBeHandledBy(admin);
systemPermission.setState(CLAIMED);
}
void claimedBy(SystemAdmin admin, SystemPermission systemPermission) {
}
|