Replace Constructors with Creation Methods

Prev don't be afraid of buying books Next

Replace Constructors with Creation Methods

Constructors on a class make it hard to decide which constructor to call during development.



Replace the constructors with intention-revealing Creation Methods that return object instances.





Motivation

Some languages allow you to name constructors any way you like, regardless of the name of the class. Other languages, such as Java and C++, don't allow this; each constructor must be named after its class. If you have one simple constructor, this may not be a problem. On the other hand, if you have multiple constructors, programmers will have to choose which constructor to call by studying the expected parameters and/or poking around at the constructor code. What's wrong with that? A lot.

Constructors simply don't communicate intention efficiently or effectively. The more constructors you have, the easier it is for programmers to choose the wrong one. Having to choose which constructor to call slows down development, and the code that does call one of the many constructors often fails to sufficiently communicate the nature of the object being constructed.

If you need to add a new constructor to a class with the same signature as an existing constructor, you're out of luck. Because they have to share the same name, you can't add the new constructor—since it isn't possible to have two constructors with the same signature in the same class, despite the fact that they would create different kinds of objects.

It's common, particularly on mature systems, to find numerous constructors that are no longer being used yet continue to live on in the code. Why are these dead constructors still present? Most of the time it's because programmers don't know that the constructors have no caller. Either they haven't checked for callers (perhaps because the search expression they'd need to formulate is too complicated) or they aren't using a development environment that automatically highlights uncalled code. Whatever the reason, dead constructors only bloat a class and make it more complicated than it needs to be.

A Creation Method can help make these problems go away. A Creation Method is simply a static or nonstatic method on a class that instantiates new instances of the class. There are no name constraints on Creation Methods, so you can name them to clearly express what you are creating (e.g., createTermLoan() or createRevolver()). This naming flexibility means that two differently named Creation Methods can accept the same number and type of arguments. And for programmers who lack modern development environments, it's usually easier to find dead Creation Method code than it is to find dead constructor code because the search expressions on specifically named methods are easier to formulate than the search expressions on one of a group of constructors.

One liability of this refactoring is that it may introduce a nonstandard way to perform creation. If most of your classes are instantiated using new yet some are instantiated using a Creation Method, programmers will have to learn how creation gets done for each class. However, this nonstandard technique for creation may be a lesser evil than having classes with too many constructors.

After you have identified a class that has many constructors, it's best to consider applying Extract Class [F] or Extract Subclass [F] before you decide to apply this refactoring. Extract Class is a good choice if the class in question is simply doing too much work (i.e., it has too many responsibilities). Extract Subclass is a good choice if instances of the class use only a small portion of the class's instance variables.

Creation Methods and Factory Methods

What does our industry call a method that creates objects? Many programmers would answer "Factory Method," after the name given to a creational pattern in Design Patterns [DP]. But are all methods that create objects true Factory Methods? Given a broad definition of the term (i.e., a method that simply creates objects), the answer would be an emphatic "yes!" But given the way the authors of the Factory Method pattern wrote it in 1995, it's clear that not every method that creates objects offers the kind of loose coupling provided by a genuine Factory Method (e.g., see Introduce Polymorphic Creation with Factory Method, 88).

To help us be clear when discussing designs or refactorings related to object creation, I'm using the term Creation Method to refer to a static or nonstatic method that creates instances of a class. This means that every Factory Method is a Creation Method but not necessarily the reverse. It also means that you can substitute the term Creation Method wherever Martin Fowler uses the term "factory method" in Refactoring [F] and wherever Joshua Bloch uses the term "static factory method" in Effective Java [Bloch].




Benefits and Liabilities

+

Communicates what kinds of instances are available better than constructors.

+

Bypasses constructor limitations, such as the inability to have two constructors with the same number and type of arguments.

+

Makes it easier to find unused creation code.

Makes creation nonstandard: some classes are instantiated using new, while others use Creation Methods.







Mechanics

Before beginning this refactoring, identify the catch-all constructor, a full-featured constructor to which other constructors delegate their work. If you don't have a catch-all constructor, create one by applying Chain Constructors (340).

1. Find a client that calls a class's constructor in order to create a kind of instance. Apply Extract Method [F] on the constructor call to produce a public, static method. This new method is a creation method. Now apply Move Method [F] to move the creation method to the class containing the chosen constructor.

  • Compile and test.

2. Find all callers of the chosen constructor that instantiate the same kind of instance as the creation method and update them to call the creation method.

  • Compile and test.

3. If the chosen constructor is chained to another constructor, make the creation method call the chained constructor instead of the chosen constructor. You can do this by inlining the constructor, a refactoring that resembles Inline Method [F].

  • Compile and test.

4. Repeat steps 1–3 for every constructor on the class that you'd like to turn into a Creation Method.

5. If a constructor on the class has no callers outside the class, make it non-public.

  • Compile.

Example

This example is inspired from the banking domain and a certain loan risk calculator I spent several years writing, extending, and maintaining. The Loan class had numerous constructors, as shown in the following code.

 public class Loan...    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 customerRating, Date maturity, Date expiry) {       this(null, commitment, outstanding, customerRating, 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();       }    } 

Loan could be used to represent seven kinds of loans. I will discuss only three of them here. A term loan is a loan that must be fully paid by its maturity date. A revolver, which is like a credit card, is a loan that signifies "revolving credit": you have a spending limit and an expiry date. A revolving credit term loan (RCTL) is a revolver that transforms into a term loan when the revolver expires.

Given that the calculator supported seven kinds of loans, you might wonder why Loan wasn't an abstract superclass with a subclass for each kind of loan. After all, that would have cut down on the number of constructors needed for Loan and its subclasses. There were two reasons why this was not a good idea.

  1. What distinguishes the different kinds of loans is not so much their fields but how numbers, like capital, income, and duration, are calculated. To support three different ways to calculate capital for a term loan, we wouldn't want to create three different subclasses of Loan. It's easier to support one Loan class and have three different Strategy classes for a term loan (see the example from Replace Conditional Logic with Strategy, 129).

  2. The application that used Loan instances needed to transform loans from one kind of loan to another. This transformation was easier to do when it involved changing a few fields on a single Loan instance, rather than completely changing one instance of a Loan subclass into another.

If you look at the Loan source code presented earlier, you'll see that it has five constructors, the last of which is its catch-all constructor (see Chain Constructors, 340). Without specialized knowledge, it is difficult to know which constructors create term loans, which ones create revolvers, and which ones create RCTLs.

I happen to know that an RCTL needs both an expiry date and a maturity date, so I know that to create an RCTL, I must call a constructor that lets me pass in both dates. Did you know that? Do you think the next programmer who reads this code will know it?

What else is embedded as implicit knowledge in the Loan constructors? Plenty. If you call the first constructor, which takes three parameters, you'll get back a term loan. But if you want a revolver, you'll need to call one of the constructors that take two dates and then supply null for the maturity date. I wonder if all users of this code will know this? Or will they just have to learn by encountering some ugly defects?

Let's see what happens when I apply the Replace Constructors with Creation Methods refactoring.

1. My first step is to find a client that calls one of Loan's constructors. Here is one such caller that resides in a test case:

 public class CapitalCalculationTests...    public void testTermLoanNoPayments() {       ...       Loan termLoan = new Loan(commitment, riskRating, maturity);       ...    } 

In this case, a call to the above Loan constructor produces a term loan. I apply Extract Method [F] on that call to produce a public, static method called createTermLoan:

 public class CapitalCalculationTests...    public void testTermLoanNoPayments() {       ...       Loan termLoan =  createTermLoan(commitment, riskRating, maturity);       ...    }     public static Loan createTermLoan(double commitment, int riskRating, Date maturity) {        return new Loan(commitment, riskRating, maturity);     } 

Next, I apply Move Method [F] on the creation method, createTermLoan, to move it to Loan. This produces the following changes:

 public class Loan...     public static Loan createTermLoan(double commitment, int riskRating, Date maturity) {        return new Loan(commitment, riskRating, maturity);     } public class CapitalCalculationTest...    public void testTermLoanNoPayments() {       ...       Loan termLoan =  Loan.createTermLoan(commitment, riskRating, maturity);       ...    } 

I compile and test to confirm that everything works.

2. Next, I find all callers on the constructor that createTermLoan calls, and I update them to call createTermLoan. For example:

 public class CapitalCalculationTest...    public void testTermLoanOnePayment() {       ...         Loan termLoan = new Loan(commitment, riskRating, maturity);        Loan termLoan = Loan.createTermLoan(commitment, riskRating, maturity);       ...    } 

Once again, I compile and test to confirm that everything is working.

3. The createTermLoan method is now the only caller on the constructor. Because this constructor is chained to another constructor, I can remove it by applying Inline Method [F] (which, in this case, is actually "inline constructor"). This leads to the following changes:

 public class Loan...      public Loan(double commitment, int riskRating, Date maturity) {         this(commitment, 0.00, riskRating, maturity, null);      }    public static Loan createTermLoan(double commitment, int riskRating, Date maturity) {       return  new Loan(commitment, 0.00, riskRating, maturity, null);    } 

I compile and test to confirm that the change works.

4. Now I repeat steps 1–3 to produce additional creation methods on Loan. For example, here is some code that calls Loan's catch-all constructor:

 public class CapitalCalculationTest...    public void testTermLoanWithRiskAdjustedCapitalStrategy() {       ...       Loan termLoan = new Loan(riskAdjustedCapitalStrategy, commitment,                                outstanding, riskRating, maturity, null);       ...    } 

Notice the null value that is passed in as the last parameter to the constructor. Passing in null values to a constructor is bad practice. It reduces the code's readability. It usually happens because programmers can't find the exact constructor they need, so instead of creating yet another constructor they call a more general-purpose one.

To refactor this code to use a creation method, I'll follow steps 1 and 2. Step 1 leads to another createTermLoan method on Loan:

 public class CapitalCalculationTest...    public void testTermLoanWithRiskAdjustedCapitalStrategy() {       ...       Loan termLoan =  Loan.createTermLoan(riskAdjustedCapitalStrategy, commitment,                                           outstanding, riskRating, maturity  , null);       ...    } public class Loan...    public static Loan createTermLoan(double commitment, int riskRating, Date maturity) {       return new Loan(commitment, 0.00, riskRating, maturity, null);    }     public static Loan createTermLoan(CapitalStrategy riskAdjustedCapitalStrategy,        double commitment, double outstanding, int riskRating, Date maturity) {        return new Loan(riskAdjustedCapitalStrategy, commitment,           outstanding, riskRating, maturity, null);     } 

Why did I choose to overload createTermLoan(…) instead of producing a creation method with a unique name, like createTermLoanWithStrategy(…)? Because I felt that the presence of the CapitalStrategy parameter sufficiently communicated the difference between the two overloaded versions of createTermLoan(…).

Now for step 2 of the refactoring. Because the new createTermLoan(…) calls Loan's catch-all constructor, I must find other clients that call the catch-all constructor to instantiate the same kind of Loan produced by createTermLoan(…). This requires careful work because some callers of the catch-all constructor produce revolver or RCTL instances of Loan. So I update only the client code that produces term loan instances of Loan.

I don't have to perform any work for step 3 because the catch-all constructor isn't chained to any other constructors. I continue to implement step 4, which involves repeating steps 1–3. When I'm done, I end up with the following creation methods:



5. The last step is to change the visibility of the only remaining public constructor, which happens to be Loan's catch-all constructor. Since it has no subclasses and it now has no external callers, I make it private:

 public class Loan...     private Loan(CapitalStrategy capitalStrategy, double commitment,                 double outstanding, int riskRating,                 Date maturity, Date expiry)... 

I compile to confirm that everything still works. The refactoring is complete.

It's now clear how to obtain different kinds of Loan instances. The ambiguities have been revealed and the implicit knowledge has been made explicit. What's left to do? Well, because the creation methods take a fairly large number of parameters, it may make sense to apply Introduce Parameter Object [F].

Variations

Parameterized Creation Methods

As you consider implementing Replace Constructors with Creation Methods, you may calculate in your head that you'd need something on the order of 50 Creation Methods to account for every object configuration supported by your class. Writing 50 methods doesn't sound like much fun, so you may decide not to apply this refactoring. Keep in mind that there are other ways to handle this situation. First, you need not produce a Creation Method for every object configuration: you can write Creation Methods for the most popular configurations and leave some public constructors around to handle the rest of the cases. It also makes sense to consider using parameters to cut down on the number of Creation Methods.

Extract Factory

Can too many Creation Methods on a class obscure its primary responsibility? This is really a matter of taste. Some folks find that when object creation begins to dominate the public interface of a class, the class no longer strongly communicates its main purpose. If you're working with a class that has Creation Methods on it and you find that the Creation Methods distract you from the primary responsibilities of the class, you can refactor the related Creation Methods to a single Factory, like so:



It's worth noting that LoanFactory is not an Abstract Factory [DP]. Abstract Factories can be substituted at runtime—you can define different Abstract Factory classes, each of which knows how to return a family of products, and you can outfit a system or client with a particular Abstract Factory instance. Factories tend to be less sophisticated. They are often implemented as a single class that is not part of any hierarchy.

Amazon


Refactoring to Patterns (The Addison-Wesley Signature Series)
Refactoring to Patterns
ISBN: 0321213351
EAN: 2147483647
Year: 2003
Pages: 103

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net