Industrial Logic -> eXtreme Programming (XP) -> Refactoring To Patterns

Refactoring To Patterns

Author: Joshua Kerievsky
Copyright (c) 2000,  Joshua Kerievsky, Industrial Logic, Inc.  All Rights Reserved.

Our Need to Refactor

If you pair-program on an Extreme Programming project, you'll often hear exchanges like this:
  • I don't think that code is intention revealing. Let's refactor it.
  • I've seen the same code over in the XYZ class. Let's refactor so we're not violating once and only once.
  • This code is too hard to follow. Let's refactor it.
The above statements reveal needs. We need our code to be readable by other programmers. We need it to be duplication-free (since duplicate code bloats a system).

Needs, therefore, drive us towards refactorings.

What I am going to show in this paper are needs that would lead you to refactor to patterns. This is a more disciplined way of using patterns than has previously been defined.

Refactoring to Factory Method

Problems:
  1. Constructor Overload: clients have to decide amoung too many many constructors on one class.
  2. Subclass Overload: clients have to know about too many subclasses.
  3. Parameter Overload: clients have to pass in the same parameters into an FM everytime they call it.

Constructor Overload

Consider a class A, which has many different constructors, say 4 or more. If you are programming and need an instance of class A, you have to decide which of the 4 or more constructors to use. Each of A's constructors will create an instance of A, but each will do so in a different way. Our job as programmers is to decide which constructor to call at the right time in our programs.

Now, the question is, is it ok to write classes that have many constructors? Is it good design? Or does it make the code hard to understand? To use an expression from Extreme Programming, "is the code intention revealing."

To answer that question, it'll help to look at an example. We'll look at some code from the financial domain (mainly because I always use examples from the financial domain, having spent a wee too many years programming on Wall Street).
 
1   public class Loan implements LoanTypes {
2     private String type;
3     private ROCStrategy roc;
4     private float notional;
5     private float outstanding;
6     private int customerRating;
7     private Date maturityDate;
8     private Date expiryDate;
9    
10     public Loan(float notional, float outstanding, int customerRating, Date expiryDate) {
11       this(TERM_LOAN, new TermROC(), notional, outstanding, customerRating, expiryDate, null);
12     }
13    
14     public Loan(float notional, float outstanding, int customerRating, Date expiryDate, Date maturityDate) {
15       this(REVOLVING_TERM, new RevolvingTermROC(), notional, outstanding, customerRating, expiryDate, maturityDate);
16     }
17    
18     public Loan(ROCStrategy roc, float notional, float outstanding, int customerRating, Date expiryDate, Date maturityDate) {
19       this(REVOLVING_TERM, roc, notional, outstanding, customerRating, expiryDate, maturityDate);
20     }
21    
22     public Loan(String type, ROCStrategy roc, float notional, float outstanding, int customerRating, Date expiryDate) {
23       this(type, roc, notional, outstanding, customerRating, expiryDate, null);
24     }
25    
26     public Loan(String type, ROCStrategy roc, float notional, float outstanding, int customerRating, Date expiryDate, Date maturityDate) {
27       this.type = type;
28       this.roc = roc;
29       this.notional = notional;
30       this.outstanding = outstanding;
31       this.customerRating = customerRating;
32       this.expiryDate = expiryDate;
33       if (type.equals(REVOLVING_TERM))
34         this.maturityDate = maturityDate;
35     }
36    
37     // additional Loan methods not shown
38   }
39    
40   abstract public class ROCStrategy {
41     protected ROCStrategy() {
42       super();
43     }
44     public float calc(Loan loan);
45   }
46    
47   public class TermROC extends ROCStrategy {
48     public TermROC() { super(); }
49     public float calc(Loan loan) {
50       // do some calculation
51     }
52   }
53    
54   public class RevolverROC extends ROCStrategy {
55     public RevolverROC() { super(); }
56     public float calc(Loan loan) {
57       // do some calculation
58     }
59   }
60    
61   public class RevolvingTermROC extends ROCStrategy {
62     public RevolvingTermROC() { super(); }
63     public float calc(Loan loan) {
64       return 0;
65     }
66   }
 
As you can see, the Loan class has 5 constructors. These five constructors are there to allow clients to create a variety of Loan instances, all of which correspond to specific loan types. (In this case, we do not have subclasses for the various loan types, but we'll see that case in the Subclass Overload section below).

The loans types supported by Loan's various constructors are Term Loans, Revolvers, and Revolving Term Loans. Each of these loan types needs its own ROC (Return on Capital) calculation, which is supplied using the Strategy pattern.

So how do you find this code? It is easy to follow? A little hard to read? To get an idea of how this code would get used in a program, let's take a look at a program which uses the Loan class to create various Loan instances. The code we're about to see is actually the JUnit test code for this example.
 
1   public class LoanCreationTest extends TestCase {
2     private final static int CUSTOMER_RATING = 2;
3     private Date expiryDate;
4     public LoanCreationTest(String name) {
5       super(name);
6     }
7     protected void setUp() throws Throwable {
8       super.setUp();
9       Calendar cal = Calendar.getInstance();
10       cal.set(2001, Calendar.NOVEMBER, 20);
11       expiryDate = cal.getTime();
12     }
13     public void testRevolverTermLoan() {
14       Calendar cal = Calendar.getInstance();
15       cal.set(2004, Calendar.NOVEMBER, 20);
16       Date maturityDate = cal.getTime();
17       Loan loan = new Loan(1000f, 250f, CUSTOMER_RATING, expiryDate, maturityDate);
18       assertNotNull(loan);
19       assertEquals(Loan.REVOLVING_TERM, loan.getType());
20       assertNotNull(loan.getMaturityDate());
21     }
22     public void testRevolverTermLoanWithStrategy() {
23       Calendar cal = Calendar.getInstance();
24       cal.set(2004, Calendar.NOVEMBER, 20);
25       Date maturityDate = cal.getTime();
26       Loan loan = new Loan(Loan.REVOLVING_TERM, new RevolvingTermROC(), 1000f, 250f, CUSTOMER_RATING, expiryDate, maturityDate);
27       assertNotNull(loan);
28       assertEquals(Loan.REVOLVING_TERM, loan.getType());
29       assertNotNull(loan.getMaturityDate());
30     }
31     public void testRevolverWithStrategy() {
32       Loan loan = new Loan(Loan.REVOLVER, new RevolverROC(), 1000f, 250f, CUSTOMER_RATING, expiryDate);
33       assertNotNull(loan);
34       assertEquals(Loan.REVOLVER, loan.getType());
35     }
36     public void testTermLoan() {
37       Loan loan = new Loan(1000f, 250f, CUSTOMER_RATING, expiryDate);
38       assertNotNull(loan);
39       assertEquals(Loan.TERM_LOAN, loan.getType());
40     }
41     public void testTermLoanWithStrategy() {
42       Loan loan = new Loan(Loan.TERM_LOAN, new TermROC(), 1000f, 250f, CUSTOMER_RATING, expiryDate);
43       assertNotNull(loan);
44       assertEquals(Loan.TERM_LOAN, loan.getType());
45     }
46   }
 
So, as you can see, the test code checks that it can instantiate each of the 3 loan types. In some cases, we want to create a loan instance and not worry about which ROCStrategy we used. In other cases, a specific loan type and concrete ROCStrategy is selected.

This gives you some idea of how client code would make calls to the Loan class constructors.

Now, as I've said, I don't find this code to be very intention revealing. When I'm coding, and I'm in need of a Loan instance, I don't like to have to go fishing around for the right constructor in order to make sure I get the right type of loan. In addition, I don't like to have to pass in many arguments to constructors - and I really don't like having to hunt down the proper static final Strings to use during creation.

These problems lead me to refactor this code. I begin with my test code, refactoring it to make the kind of calls I would like to make to Loan -- simple, intention revealing calls. Here's the new test code.
 
1   public class LoanCreationTest extends TestCase {
2     private final static int CUSTOMER_RATING = 2;
3     private Date expiryDate;
4     public LoanCreationTest(String name) {
5       super(name);
6     }
7     protected void setUp() throws Throwable {
8       super.setUp();
9       Calendar cal = Calendar.getInstance();
10       cal.set(2001, Calendar.NOVEMBER, 20);
11       expiryDate = cal.getTime();
12     }
13     public void testRevolver() {
14       Loan loan = Loan.newRevolver(1000f, 250f, CUSTOMER_RATING, expiryDate);
15       assertNotNull(loan);
16       assertEquals(Loan.REVOLVER, loan.getType());
17     }
18     public void testRevolverTermLoan() {
19       Calendar cal = Calendar.getInstance();
20       cal.set(2004, Calendar.NOVEMBER, 20);
21       Date maturityDate = cal.getTime();
22       Loan loan = Loan.newRevolverTerm(1000f, 250f, CUSTOMER_RATING, expiryDate, maturityDate);
23       assertNotNull(loan);
24       assertEquals(Loan.REVOLVING_TERM, loan.getType());
25       assertNotNull(loan.getMaturityDate());
26     }
27     public void testRevolverTermLoanWithStrategy() {
28       Calendar cal = Calendar.getInstance();
29       cal.set(2004, Calendar.NOVEMBER, 20);
30       Date maturityDate = cal.getTime();
31       Loan loan = Loan.newRevolverTermWithStrategy(new RevolvingTermROC(), 1000f, 250f, CUSTOMER_RATING, expiryDate, maturityDate);
32       assertNotNull(loan);
33       assertEquals(Loan.REVOLVING_TERM, loan.getType());
34       assertNotNull(loan.getMaturityDate());
35     }
36     public void testRevolverWithStrategy() {
37       Loan loan = Loan.newRevolverWithStrategy(new RevolverROC(), 1000f, 250f, CUSTOMER_RATING, expiryDate);
38       assertNotNull(loan);
39       assertEquals(Loan.REVOLVER, loan.getType());
40     }
41     public void testTermLoan() {
42       Loan loan = Loan.newTermLoan(1000f, 250f, CUSTOMER_RATING, expiryDate);
43       assertNotNull(loan);
44       assertEquals(Loan.TERM_LOAN, loan.getType());
45     }
46     public void testTermLoanWithStrategy() {
47       Loan loan = Loan.newTermLoanWithStrategy(new TermROC(), 1000f, 250f, CUSTOMER_RATING, expiryDate);
48       assertNotNull(loan);
49       assertEquals(Loan.TERM_LOAN, loan.getType());
50     }
51   }
 
Notice how I am now making static calls on the Loan class, and how each call is a Factory Method, with a very intention revealing name.

Let's see what the Loan class looks like now:
 
1   public class Loan implements LoanTypes {
2     private String type;
3     private ROCStrategy roc;
4     private float notional;
5     private float outstanding;
6     private int customerRating;
7     private Date maturityDate;
8     private Date expiryDate;
9     protected Loan(String type, ROCStrategy roc, float notional, float outstanding, int customerRating, Date expiryDate, Date maturityDate) {
10       this.type = type;
11       this.roc = roc;
12       this.notional = notional;
13       this.outstanding = outstanding;
14       this.customerRating = customerRating;
15       this.expiryDate = expiryDate;
16       if (type.equals(REVOLVING_TERM))
17         this.maturityDate = maturityDate;
18     }
19     static Loan newRevolver(float notional, float outstanding, int customerRating, Date expiryDate) {
20       return new Loan(REVOLVER, new RevolverROC(), notional, outstanding, customerRating, expiryDate, null);
21     }
22     static Loan newRevolverTerm(float notional, float outstanding, int customerRating, Date expiryDate, Date maturityDate) {
23       return new Loan(REVOLVING_TERM, new RevolvingTermROC(), notional, outstanding, customerRating, expiryDate, maturityDate);
24     }
25     static Loan newRevolverTermWithStrategy(ROCStrategy roc, float notional, float outstanding, int customerRating, Date expiryDate, Date maturityDate) {
26       return new Loan(REVOLVING_TERM, roc, notional, outstanding, customerRating, expiryDate, maturityDate);
27     }
28     static Loan newRevolverWithStrategy(ROCStrategy roc, float notional, float outstanding, int customerRating, Date expiryDate) {
29       return new Loan(REVOLVER, roc, notional, outstanding, customerRating, expiryDate, null);
30     }
31     static Loan newTermLoan(float notional, float outstanding, int customerRating, Date expiryDate) {
32       return new Loan(TERM_LOAN, new TermROC(), notional, outstanding, customerRating, expiryDate, null);
33     }
34     static Loan newTermLoanWithStrategy(ROCStrategy roc, float notional, float outstanding, int customerRating, Date expiryDate) {
35       return new Loan(TERM_LOAN, roc, notional, outstanding, customerRating, expiryDate, null);
36     }
37    
38     // additional Loan methods not shown
39   }
 
All of the Factory Methods begin with the word "new", to distinguish them from Loan's other methods (not shown). In addition, the Loan class no longer has public constructors: it has only one protected constructor. This ensures that no client code will ever directly instantiate a loan.

We've now completed our refactoring of Loan's constructors. Our code is now intention revealing and a whole lot easier to work with.

Next, we'll have a look at another need that drives us to refactor to a Factory Method.

Subclass Overload

Another problem can arise that can lead you to refactor your code to use Factory Methods. This one involves subclasses. Sometimes, you can have many subclasses which all adhere to a common interface. Perhaps you've also written polymorphic code to work with your various subclasses. The trouble is, when you don't have one place to go to create one of your many subclass types, it can become hard to know which ones are available, and what they do. In addition, even on these subclasses, the "Constructor Overload" problem can occur.

We'll again revisit our Loan example. Actually, I'll come right out and tell you right now that every example we are going to see will be somehow related to Loans. I'm sorry. I'm boring and lazy.

Let's say for this example, that our Loan class has become abstract, and that we have three subclasses of Loan, called TermLoan, Revolver, and RevolverTermLoan. Now, it should be said that if you only had three subclasses, perhaps you would not feel the need to refactor to a Factory Method. I don't know if I would. But consider the case of 5 subclasses, 6, 7 or more. As the number of subclasses grows, it can become harder to keep track of which one you need to use, and how to construct each particular type.

So for this example, there will be three subclasses of Loan. (Trust me on this point: if this were a real banking application, we'd have many many more types of Loans and a fair number of ways to calculate ROC -- Return on Capital -- on each loan type). Ok, now, in the spirit of XP, I've written a test to instantiate each of the 3 loan types, and to test that I create the correct type of objects.
 
1   public class LoanCreationTest extends TestCase {
2     private final static int CUSTOMER_RATING = 2;
3     private Date expiryDate;
4     public LoanCreationTest(String name) {
5       super(name);
6     }
7     protected void setUp() throws Throwable {
8       super.setUp();
9       Calendar cal = Calendar.getInstance();
10       cal.set(2001, Calendar.NOVEMBER, 20);
11       expiryDate = cal.getTime();
12     }
13     public void testRevolver() {
14       Loan loan = new Revolver(1000f, 250f, CUSTOMER_RATING, expiryDate);
15       assertNotNull(loan);
16       assertEquals(Loan.REVOLVER, loan.getType());
17       assert(loan instanceof Revolver);
18     }
19     public void testRevolverTermLoan() {
20       Calendar cal = Calendar.getInstance();
21       cal.set(2004, Calendar.NOVEMBER, 20);
22       Date maturityDate = cal.getTime();
23       Loan loan = new RevolverTerm(1000f, 250f, CUSTOMER_RATING, expiryDate, maturityDate);
24       assertNotNull(loan);
25       assertEquals(Loan.REVOLVING_TERM, loan.getType());
26       assertNotNull(loan.getMaturityDate());
27       assert(loan instanceof RevolverTerm);
28     }
29     public void testRevolverTermLoanWithStrategy() {
30       Calendar cal = Calendar.getInstance();
31       cal.set(2004, Calendar.NOVEMBER, 20);
32       Date maturityDate = cal.getTime();
33       Loan loan = new RevolverTerm(new RevolvingTermROC(), 1000f, 250f, CUSTOMER_RATING, expiryDate, maturityDate);
34       assertNotNull(loan);
35       assertEquals(Loan.REVOLVING_TERM, loan.getType());
36       assertNotNull(loan.getMaturityDate());
37       assert(loan instanceof RevolverTerm);
38     }
39     public void testRevolverWithStrategy() {
40       Loan loan = new Revolver(new RevolverROC(), 1000f, 250f, CUSTOMER_RATING, expiryDate);
41       assertNotNull(loan);
42       assertEquals(Loan.REVOLVER, loan.getType());
43       assert(loan instanceof Revolver);
44     }
45     public void testTermLoan() {
46       Loan loan = new TermLoan(1000f, 250f, CUSTOMER_RATING, expiryDate);
47       assertNotNull(loan);
48       assertEquals(Loan.TERM_LOAN, loan.getType());
49       assert(loan instanceof TermLoan);
50     }
51     public void testTermLoanWithStrategy() {
52       Loan loan = new TermLoan(new TermROC(), 1000f, 250f, CUSTOMER_RATING, expiryDate);
53       assertNotNull(loan);
54       assertEquals(Loan.TERM_LOAN, loan.getType());
55       assert(loan instanceof TermLoan);
56     }
57   }
 
So there is nothing real fancy here. Just notice the constructor calls for the various Loan subclasses. We're no longer relying on the Loan class to represent every type of loan. Instead, we've decided to simplify Loan, and extract the various "loan types" into thier own classes. In effect, we've performed the refactoring which Martin Fowler calls Replace Type Code with Subclasses [see Fowler's book, Refactoring: Improving the Design of Existing Code].

Extracting each loan type into its own class is a good refactoring: it makes it easier to write code for each specific type, since we don't have to place unique code into a common class (i.e. Loan). You would be driven to this refactoring as your "loan type" count grew.

Here is a quick view of the new classes:
 
1   abstract public class Loan implements LoanTypes {
2     protected String type;
3     protected ROCStrategy roc;
4     protected float notional;
5     protected float outstanding;
6     protected int customerRating;
7     protected Date maturityDate;
8     protected Date expiryDate;
9     protected Loan(ROCStrategy roc, float notional, float outstanding, int customerRating, Date expiryDate, Date maturityDate) {
10       this.roc = roc;
11       this.notional = notional;
12       this.outstanding = outstanding;
13       this.customerRating = customerRating;
14       this.expiryDate = expiryDate;
15       this.maturityDate = maturityDate;
16     }
17     public float calculateROC() {
18       return roc.calc(this);
19     }
20     abstract String getType();
21    
22     // additional methods not shown
23   }
24    
25   public class TermLoan extends Loan {
26     public TermLoan(float notional, float outstanding, int customerRating, Date expiryDate) {
27       super(new TermROC(), notional, outstanding, customerRating, expiryDate, null);
28     }
29     public TermLoan(ROCStrategy roc, float notional, float outstanding, int customerRating, java.util.Date expiryDate) {
30       super(roc, notional, outstanding, customerRating, expiryDate, null);
31     }
32     public String getType() {
33       return TERM_LOAN;
34     }
35    
36     // additional methods not shown
37   }
38    
39   public class Revolver extends Loan {
40     public Revolver(float notional, float outstanding, int customerRating, Date expiryDate) {
41       super(new RevolverROC(), notional, outstanding, customerRating, expiryDate, null);
42     }
43     public Revolver(ROCStrategy roc, float notional, float outstanding, int customerRating, Date expiryDate) {
44       super(roc, notional, outstanding, customerRating, expiryDate, null);
45     }
46     public String getType() {
47       return REVOLVER;
48     }
49    
50     // additional methods not shown
51   }
52    
53   public class RevolverTerm extends Loan {
54     public RevolverTerm(float notional, float outstanding, int customerRating, Date expiryDate, Date maturityDate) {
55       super(new RevolvingTermROC(), notional, outstanding, customerRating, expiryDate, maturityDate);
56     }
57     public RevolverTerm(ROCStrategy roc, float notional, float outstanding, int customerRating, Date expiryDate, Date maturityDate) {
58       super(roc, notional, outstanding, customerRating, expiryDate, maturityDate);
59     }
60     public String getType() {
61       return REVOLVING_TERM;
62     }
63    
64     // additional methods not shown
65   }
 
The trouble is, as the number of types of Loans increases, your client code has to make more and more choices about which types to instantiate and how to instantiate them. But if your client code is always interacting with loans via the Loan interface, your client code really doesn't need to know about the existence of all the subclasses. How can we say that?

What we're talking about here is what my friend Benny Sadeh calls "Encapsulate Variance." We have various Loan subclasses -- all of which are variations of a Loan. We have clients of these Loans subclasses -- that is, client code which uses the subclass instances. Now, even though client code needs Loan instances to do certain thins, this client code still interacts with each Loan subclass via the Loan interface. In other words, client code really doesn't need direct knowledge of each Loan subclass. It just needs a way to get instances of each subclass type.

Enter Factory Method

Factory Methods can help us "Encapsulate the Variance", while preserving the existence of the Loan subclasses. Here's the code:
 
1   public abstract class Loan implements LoanTypes {
2     protected String type;
3     protected ROCStrategy roc;
4     protected float notional;
5     protected float outstanding;
6     protected int customerRating;
7     protected Date maturityDate;
8     protected Date expiryDate;
9     protected Loan(ROCStrategy roc, float notional, float outstanding, int customerRating, Date expiryDate, Date maturityDate) {
10       this.roc = roc;
11       this.notional = notional;
12       this.outstanding = outstanding;
13       this.customerRating = customerRating;
14       this.expiryDate = expiryDate;
15       this.maturityDate = maturityDate;
16     }
17     public float calculateROC() {
18       return roc.calc(this);
19     }
20     abstract String getType();
21     static Loan newRevolver(float notional, float outstanding, int customerRating, Date expiryDate) {
22       return new Revolver(notional, outstanding, customerRating, expiryDate);
23     }
24     static Loan newRevolverTerm(float notional, float outstanding, int customerRating, Date expiryDate, Date maturityDate) {
25       return new RevolverTerm(notional, outstanding, customerRating, expiryDate, maturityDate);
26     }
27     static Loan newRevolverTermWithStrategy(ROCStrategy roc, float notional, float outstanding, int customerRating, Date expiryDate, Date maturityDate) {
28       return new RevolverTerm(roc, notional, outstanding, customerRating, expiryDate, maturityDate);
29     }
30     static Loan newRevolverWithStrategy(ROCStrategy roc, float notional, float outstanding, int customerRating, Date expiryDate) {
31       return new Revolver(roc, notional, outstanding, customerRating, expiryDate);
32     }
33     static Loan newTermLoan(float notional, float outstanding, int customerRating, Date expiryDate) {
34       return new TermLoan(notional, outstanding, customerRating, expiryDate);
35     }
36     static Loan newTermLoanWithStrategy(ROCStrategy roc, float notional, float outstanding, int customerRating, Date expiryDate) {
37       return new TermLoan(roc, notional, outstanding, customerRating, expiryDate);
38     }
39     // additional methods not shown
40   }
41    
42   public class TermLoan extends Loan {
43     protected TermLoan(float notional, float outstanding, int customerRating, Date expiryDate) {
44       super(new TermROC(), notional, outstanding, customerRating, expiryDate, null);
45     }
46     protected TermLoan(ROCStrategy roc, float notional, float outstanding, int customerRating, Date expiryDate) {
47       super(roc, notional, outstanding, customerRating, expiryDate, null);
48     }
49     public String getType() {
50       return TERM_LOAN;
51     }
52     // additional methods not shown
53   }
54    
55   public class Revolver extends Loan {
56     protected Revolver(float notional, float outstanding, int customerRating, Date expiryDate) {
57       super(new RevolverROC(), notional, outstanding, customerRating, expiryDate, null);
58     }
59     protected Revolver(ROCStrategy roc, float notional, float outstanding, int customerRating, Date expiryDate) {
60       super(roc, notional, outstanding, customerRating, expiryDate, null);
61     }
62     public String getType() {
63       return REVOLVER;
64     }
65     // additional methods not shown
66   }
67    
68   public class RevolverTerm extends Loan {
69     protected RevolverTerm(float notional, float outstanding, int customerRating, Date expiryDate, Date maturityDate) {
70       super(new RevolvingTermROC(), notional, outstanding, customerRating, expiryDate, maturityDate);
71     }
72     protected RevolverTerm(ROCStrategy roc, float notional, float outstanding, int customerRating, Date expiryDate, Date maturityDate) {
73       super(roc, notional, outstanding, customerRating, expiryDate, maturityDate);
74     }
75     public String getType() {
76       return REVOLVING_TERM;
77     }
78     // additional methods not shown
79   }
 
The test code for this is not much different from the test code for the refactored version of the Constructor Overload example seen earlier. The only difference is that after instantiating each loan type, I test to see that it is the correct instance.

Let's look at some finer points of the above code. For starters, I've given the abstract Loan class 6 static newXXX Factory Methods. This allows our client code to go to one place to obtain Loan instances.

I've also changed the visibility of the subclass loan constructors. Notice that they are now all protected. This means that only the Loan class can create Loan subclasses, which it does via its Factory Methods.

We now have some pretty well factored code. An abtract Loan doesn't have to represent all three types of loans, but it can provide common functionality for all three. More importantly, Loan can be the central place to go to find intention revealing Factory Methods for the various Loan subclasses. Loan variance has thus been encapsuled which sets us up to write simple, straightforward client code.

Parameter Overload

(Explain this in plain english)

Refactoring to Strategy

Let's ask the question, "When does it make sense to refactor your code to use the Strategy pattern?"

Here are a few answers:

  • Your client code has to deal with too many choices
The real trick when refactoring to Strategy is knowing when to actually do it. You can write very simple code that will accomplishes the same thing as Strategy, without the additional objects and the need to parameterize Context classes with those objects. But a time may come when you actually need to implement Strategy. When is it appropriate? We'll look at some code examples to help us answer this question.

We'll start with a simple Loan class and a test for that class.
 
1   public class Loan {
2     protected float riskFactor;
3     protected float income;
4     protected float capital;
5     public Loan (float income, float capital) {
6       this(income, capital, 0);
7     }
8     public Loan (float income, float capital, float riskFactor) {
9       this.riskFactor = riskFactor;
10       this.income = income;
11       this.capital = capital;
12     }
13     public float calcReturnOnCapital() {
14       float ROC;
15       if (riskFactor == 0.0)
16         ROC = (income / capital);
17       else
18         ROC = (income / capital) * riskFactor;
19       return ROC;
20     }
21     public float getCapital() {
22       return capital;
23     }
24     public float getIncome() {
25       return income;
26     }
27     public float getRiskFactor() {
28       return riskFactor;
29     }
30   }
31    
32   public class LoanCalculationTest extends TestCase {
33     private final static float RISK_FACTOR = 4.5f;
34     private final static float income = 10000;
35     private final static float capital = 50000;
36    
37     public LoanCalculationTest(String name) {
38       super(name);
39     }
40     public void testTermLoan() {
41       Loan termLoan = new Loan(income, capital);
42       float expectedResult = 0.2f;
43       assertEquals(termLoan.calcReturnOnCapital(), expectedResult, 0.0);
44     }
45   }
 
Ok, so the basic idea is that we need to calculate Return on Capital (ROC) for Loans. Today, we can do this in two ways: the regular way, and the risk-adjusted way (which requires multiplying the result by a risk factor). To support both types of calculations, we use an if .. else statement in the calcReturnOnCapital method.

The if .. else statement is pretty simple, but if you look at that code, is it clear that one calculation produces risk-adjusted ROC, while the other produces standard ROC? I don't think so. Let's refactor this code to make it more intention revealing.
 
 
need Strategy? Would it be better if we gave loan the object it needs to compute Return on Capital (ROC)? Hmmmm, we only have two types of ROC calculations -- does that merit the creation of 2 concrete Strategy classes, and perhaps an abstract class or interface to represent their abstraction?

I don't think so. But something does need to be done to make the intention of this code more revealing.

Refactoring to Command

Command is one of my favorite patterns. I've used it to drastically simplify code, to make extremely flexible code, and to write highly dynamic user-driven software. However, since I like it so much, I may've been a little over zealous about using it. I'm healed of that habit now. I refactor to Command when the opportunity presents itself.

And the opportunity did recently present itself on an XP project I've been programming on. I've attempted to duplicate the example from this project, using a different domain.

So without further ado, let's look at some sample code. The following is a Loan class, which has a number of calculation methods on it. Following this, is some sample client code which makes calls to a Loan instance's calculation methods.
 
1   public class Loan {
2    private float notional;
3    private float outstanding;
4    private int customerRating;
5    private Date startDate;
6    private Date expiryDate;
7     public void Loan(float notional, float outstanding, int customerRating, Date startDate, Date expiryDate) {
8       this.notional = notional;
9       this.outstanding = outstanding;
10       this.customerRating = customerRating;
11       this.startDate = startDate;
12       this.expiryDate = expiryDate;
13     }
14     public float calcCapital(Date asOfDate) {
15       return calcRiskAmount(asOfDate) * calcDuration(asOfDate);
16     }
17     public float calcDuration(Date asOfDate) {
18       final int DAY = 86400000;
19       long millis = asOfDate.getTime() - startDate.getTime();
20       return (millis/DAY)/365;
21     }
22     public float calcIncome(Date asOfDate) {
23       return notional;
24     }
25     public float calcReturnOnCapital(Date asOfDate) {
26       float capital = calcCapital(asOfDate);
27       return (capital != 0) ? calcIncome(asOfDate) / calcCapital(asOfDate) : 0;
28     }
29     public float calcRiskAdjReturnOnCapital(Date asOfDate) {
30       return calcReturnOnCapital(asOfDate) * RiskFactors.getInstance().rating(customerRating);
31     }
32     public float calcRiskAmount(Date asOfDate) {
33       return getOutstanding();
34     }
35     // additional methods now shown
36   }
37    
38   public class Client {
39    private final static int CUSTOMER_RATING = 2;
40     static void main(String[] args) {
41       Calendar cal = Calendar.getInstance();
42       Date startDate = cal.getTime();
43       cal.set(2001, Calendar.NOVEMBER, 20);
44       Date expiryDate = cal.getTime();
45    
46       Loan loan = new Loan(1000f, 250f, CUSTOMER_RATING, startDate, expiryDate);
47    
48       System.out.println("Loan Report\n");
49    
50       System.out.println("Beginning Loan Amounts");
51       System.out.println("\tRisk Amount:\t" + loan.calcRiskAmount(startDate));
52       System.out.println("\tIncome:\t" + loan.calcIncome(startDate));
53       System.out.println("\tDuration:\t" + loan.calcDuration(startDate));
54       System.out.println("\tCapital:\t" + loan.calcCapital(startDate));
55       System.out.println("\tROC:\t" + loan.calcReturnOnCapital(startDate));
56       System.out.println("\tRAROC:\t" + loan.calcRiskAdjReturnOnCapital(startDate));
57    
58       System.out.println("\nEnding Loan Amounts");
59       System.out.println("\tRisk Amount:\t" + loan.calcRiskAmount(expiryDate));
60       System.out.println("\tIncome:\t" + loan.calcIncome(expiryDate));
61       System.out.println("\tDuration:\t" + loan.calcDuration(expiryDate));
62       System.out.println("\tCapital:\t" + loan.calcCapital(expiryDate));
63       System.out.println("\tROC:\t" + loan.calcReturnOnCapital(expiryDate));
64       System.out.println("\tRAROC:\t" + loan.calcRiskAdjReturnOnCapital(expiryDate));
65     }
66   }
 
Ok, so this client code is perfectly harmless. There is some repitition in the code, but we could easily fix that with a general method to print out values for all the calculations for a given date.

But there is a larger problem, which can manifest itself when you look at how the Loan class is used in a real system. Consider that we have a fair number of calculation methods on the Loan class. These methods tend to get called in groups - we'll calculate Risk Amount and Duration and Capital, and somewhere else in the system, we'll calculate Return On Capital and Risk-Adjusted Return On Capital. So, every place where we call Loan calculation methods, we need to hard-code the method calls into our client code. This can lead to a lot of code which is generally doing the same thing - calling a Loan calculation method and using the result for some purpose.

We can improve this design using Command. Let's look at how this code looks when it uses the Command pattern.
 
1   public interface LoanCalcs {
2    public final static String RISK_AMOUNT = "Risk Amount";
3    public final static String INCOME = "Income";
4    public final static String DURATION = "Duration";
5    public final static String CAPITAL = "Capital";
6    public final static String ROC = "ROC";
7    public final static String RAROC = "RAROC";
8   }
9    
10   public class Loan implements LoanCalcs {
11    private float notional;
12    private float outstanding;
13    private int customerRating;
14    private Date startDate;
15    private Date expiryDate;
16    private Hashtable calculations;
17    private interface LoanCalculation {
18     public float calc(Loan loan, Date asOfDate);
19    }
20     public Loan(float notional, float outstanding, int customerRating, Date startDate, Date expiryDate) {
21       this.notional = notional;
22       this.outstanding = outstanding;
23       this.customerRating = customerRating;
24       this.startDate = startDate;
25       this.expiryDate = expiryDate;
26     }
27     public float calc(String calculationName, Date asOfDate) {
28       LoanCalculation loanCalc = getCalc(calculationName);
29       return loanCalc.calc(this, asOfDate);
30     }
31     public LoanCalculation getCalc(String calcName) {
32       if (calculations == null) {
33         calculations = new Hashtable();
34    
35         calculations.put(CAPITAL, new LoanCalculation() {
36           public float calc(Loan loan, Date asOfDate) {
37             return loan.calc(RISK_AMOUNT, asOfDate) * loan.calc(DURATION, asOfDate);
38           }
39         });
40    
41         calculations.put(RISK_AMOUNT, new LoanCalculation() {
42           public float calc(Loan loan, Date asOfDate) {
43             return loan.getOutstanding();
44           }
45         });
46    
47         calculations.put(DURATION, new LoanCalculation() {
48           public float calc(Loan loan, Date asOfDate) {
49             final int DAY = 86400000;
50             long millis = asOfDate.getTime() - loan.startDate.getTime();
51             return (millis/DAY)/365;
52           }
53         });
54    
55         calculations.put(INCOME, new LoanCalculation() {
56           public float calc(Loan loan, Date asOfDate) {
57             return loan.notional;
58           }
59         });
60    
61         calculations.put(ROC, new LoanCalculation() {
62           public float calc(Loan loan, Date asOfDate) {
63             float capital = loan.calc(CAPITAL, asOfDate);
64             return (capital != 0) ? loan.calc(INCOME, asOfDate) / capital : 0;
65           }
66         });
67    
68         calculations.put(RAROC, new LoanCalculation() {
69           public float calc(Loan loan, Date asOfDate) {
70             return loan.calc(ROC, asOfDate) * RiskFactors.getInstance().rating(customerRating);
71           }
72         });
73    
74       }
75       return (LoanCalculation)calculations.get(calcName);
76     }
77   }
78    
79   public class Client {
80     static void main(String[] args) {
81       Calendar cal = Calendar.getInstance();
82       Date startDate = cal.getTime();
83       cal.set(2001, Calendar.NOVEMBER, 20);
84       Date expiryDate = cal.getTime();
85    
86       Loan loan = new Loan(1000f, 250f, CUSTOMER_RATING, startDate, expiryDate);
87       String[] calcs = new String[] {
88         LoanCalcs.RISK_AMOUNT,
89         LoanCalcs.INCOME,
90         LoanCalcs.DURATION,
91         LoanCalcs.CAPITAL,
92         LoanCalcs.ROC,
93         LoanCalcs.RAROC
94       };
95    
96       System.out.println("Loan Report\n");
97    
98       System.out.println("Beginning Loan Amounts");
99       for (int i=0; i < calcs.length; i++)
100         System.out.println("\t" + calcs[i] + ":\t" + loan.calc(calcs[i], startDate));
101    
102       System.out.println("\nEnding Loan Amounts");
103       for (int i=0; i < calcs.length; i++)
104         System.out.println("\t" + calcs[i] + ":\t" + loan.calc(calcs[i], expiryDate));
105     }
106   }
 

  Industrial Logic, Inc.

 


FacebookFacebook  TwitterTwitter  linked inLinkedIn