Form Template Method

Prev don't be afraid of buying books Next

Form Template Method

Two methods in subclasses perform similar steps in the same order, yet the steps are different.



Generalize the methods by extracting their steps into methods with identical signatures, then pull up the generalized methods to form a Template Method.





Motivation

Template Methods "implement the invariant parts of an algorithm once and leave it up to subclasses to implement the behavior that can vary" [DP, 326]. When invariant and variant behaviors are mixed together in the subclass implementations of a method, the invariant behavior is duplicated in the subclasses. Refactoring to a Template Method helps rid subclasses of their duplicated invariant behavior by moving the behavior to one place: a generalized algorithm in a superclass method.

A Template Method's invariant behavior consists of the following:

  • Methods called and the ordering of those methods

  • Abstract methods that subclasses must override

  • Hook methods (i.e., concrete methods) that subclasses may override

For example, consider the following code:

 public abstract class Game...    public void initialize() {       deck = createDeck();       shuffle(deck);       drawGameBoard();       dealCardsFrom(deck);    }    protected abstract Deck createDeck();    protected void shuffle(Deck deck) {       ...shuffle implementation    }    protected abstract void drawGameBoard();    protected abstract void dealCardsFrom(Deck deck); 

The list of methods called by and ordered within initialize() is invariant. The fact that subclasses must override the abstract methods is also invariant. The shuffle() implementation provided by Game is not invariant: it's a hook method that lets subclasses inherit behavior or vary it by overriding shuffle().

Because it is too tedious to implement many methods just to flesh out a Template Method in a subclass, the authors of Design Patterns [DP] suggest that a Template Method should minimize the number of abstract methods classes must override. There's also no simple way for programmers to know which methods may be overridden (i.e., hook methods) without studying the contents of a Template Method.

Template Methods often call Factory Methods [DP], like createDeck() in the above code. The refactoring Introduce Polymorphic Creation with Factory Method (88) provides a real-world example of this.

Languages like Java allow you to declare a Template Method final, which prevents accidental overriding of the Template Method by subclasses. In general, this is done only if client code in a system or framework totally relies on the invariant behavior in a Template Method and if allowing reinterpretation of that invariant behavior could cause client code to work incorrectly.

Martin Fowler's Form Template Method [F] and my version cover much of the same ground and can be thought of as the same refactoring. My mechanics use different terminology and have a different final step than Martin's. In addition, the code I've discussed in the Example section illustrates a case where the invariant behavior duplicated in subclasses is subtle, whereas Martin's example deals with a case where such duplication is explicit. If you aren't familiar with the Template Method pattern, you'd do well to study both versions of this refactoring.

Benefits and Liabilities

+

Removes duplicated code in subclasses by moving invariant behavior to a superclass.

+

Simplifies and effectively communicates the steps of a general algorithm.

+

Allows subclasses to easily customize an algorithm.

Complicates a design when subclasses must implement many methods to flesh out the algorithm.







Mechanics

1. In a hierarchy, find a similar method (a method in a subclass that performs similar steps in a similar order to a method in another subclass). Apply Compose Method (123) on the similar method (in both subclasses), extracting identical methods (methods that have the same signature and body in each subclass) and unique methods (methods that have a different signature and body in each subclass).

When deciding whether to extract code as a unique method or an identical method, consider this: If you extract the code as a unique method, you'll eventually (during step 5) need to produce an abstract or concrete version of that unique method in the superclass. Will it make sense for subclasses to inherit or override the unique method? If not, extract the code into an identical method.

2. Pull up the identical methods to the superclass by applying Pull Up Method [F].

3. To produce an identical body for each version of the similar method, apply Rename Method [F] on every unique method until the similar method is identical in each subclass.

  • Compile and test after each application of Rename Method [F].

4. If the similar method doesn't already have an identical signature in each subclass, apply Rename Method [F] to produce an identical signature.

5. Apply Pull Up Method [F] on the similar method (in either subclass), defining abstract methods on the superclass for each unique method. The pulled-up similar method is now a Template Method.

  • Compile and test.

Example

At the end of the example used in this catalog for the refactoring Replace Conditional Logic with Strategy (129) there are three subclasses of the abstract class, CapitalStrategy:



These three subclasses happen to contain a small amount of duplication, which, as we'll see in this section, can be removed by applying Form Template Method. It is relatively common to combine the Strategy and Template Method patterns to produce concrete Strategy classes that have little or no duplicate code in them.

The CapitalStrategy class defines an abstract method for the capital calculation:

 public abstract class CapitalStrategy...    public abstract double capital(Loan loan); 

Subclasses of CapitalStrategy calculate capital similarly:

 public class CapitalStrategyAdvisedLine...    public double capital(Loan loan) {       return loan.getCommitment() * loan.getUnusedPercentage() *              duration(loan) * riskFactorFor(loan);    } public class CapitalStrategyRevolver...    public double capital(Loan loan) {       return (loan.outstandingRiskAmount() * duration(loan) * riskFactorFor(loan))            + (loan.unusedRiskAmount() * duration(loan) * unusedRiskFactor(loan));    } public class CapitalStrategyTermLoan...    public double capital(Loan loan) {       return loan.getCommitment() * duration(loan) * riskFactorFor(loan);    }    protected double duration(Loan loan) {       return weightedAverageDuration(loan);    }    private double weightedAverageDuration(Loan loan)... 

I observe that CapitalStrategyAdvisedLine's calculation is identical to CapitalStrategyTermLoan's calculation, except for a step that multiplies the result by the loan's unused percentage (loan.getUnusedPercentage()). Spotting this similar sequence of steps with a slight variation means I can generalize the algorithm by refactoring to Template Method. I'll do that in the following steps and then deal with the third class, CapitalStrategyRevolver, at the end of this Example section.

1. The capital(…) method implemented by CapitalStrategyAdvisedLine and CapitalStrategyTermLoan is the similar method in this example.

The mechanics direct me to apply Compose Method (123) on the capital() implementations by extracting identical methods or unique methods. Since the formulas in capital() are identical except for CapitalStrategyAdvisedLine's step of multiplying by loan.getUnusedPercentage(), I must choose whether to extract that step into its own unique method or extract it as part of a method that includes other code. The mechanics work either way. In this case, several years of programming loan calculators for a bank aids me in making a decision. The risk amount for an advised line is calculated by multiplying the loan's commitment amount by its unused percentage (i.e., loan.getCommitment() * loan.getUnusedPercentage()). In addition, I know the standard formula for risk-adjusted capital:

Risk Amount x Duration x Risk Factor



That knowledge leads me to extract the CapitalStrategyAdvisedLine code, loan.getCommitment() * loan.getUnusedPercentage(), into its own method, riskAmountFor(), while performing a similar step for CapitalStrategyTermLoan:

 public class CapitalStrategyAdvisedLine...    public double capital(Loan loan) {       return  riskAmountFor(loan) * duration(loan) * riskFactorFor(loan);    }     private double riskAmountFor(Loan loan) {        return loan.getCommitment() * loan.getUnusedPercentage();     } public class CapitalStrategyTermLoan...    public double capital(Loan loan) {       return  riskAmountFor(loan) * duration(loan) * riskFactorFor(loan);    }     private double riskAmountFor(Loan loan) {        return loan.getCommitment();     } 

Domain knowledge clearly influenced my refactoring decisions during this step. In his book Domain-Driven Design [Evans], Eric Evans describes how domain knowledge often directs what we choose to refactor or how we choose to refactor it.

2. This step asks me to pull up identical methods to the superclass, CapitalStrategy. In this case, the riskAmountFor(…) method is not an identical method because the code in each implementation of it varies, so I can move on to the next step.

3. Now I must ensure that any unique methods have the same signature in each subclass. The only unique method, riskAmountFor(…), already has the same signature in each subclass, so I can proceed to the next step.

4. I must now ensure that the similar method, capital(…), has the same signature in both subclasses. It does, so I proceed to the next step.

5. Because the capital(…) method in each subclass now has the same signature and body, I can pull it up to CapitalStrategy by applying Pull Up Method [F]. This involves declaring an abstract method for the unique method, riskAmountFor(…):

 public abstract class CapitalStrategy...      public abstract double capital(Loan loan);     public double capital(Loan loan) {        return riskAmountFor(loan) * duration(loan) * riskFactorFor(loan);     }     public abstract double riskAmountFor(Loan loan); 

The capital() method is now a Template Method. That completes the refactoring for the CapitalStrategyAdvisedLine and CapitalStrategyTermLoan subclasses.

Before I handle the capital calculation in CapitalStrategyRevolver, I'd like to show what would have happened had I not created a riskAmountFor(…) method during step 1 of the refactoring. In that case, I would have created a unique method for CapitalStrategyAdvisedLine's step of multiplying by loan.getUnusedPercentage(). I would have called such a step unusedPercentageFor(…) and implemented it as a hook method in CapitalStrategy:

 public abstract class CapitalStrategy...    public double capital(Loan loan) {       return loan.getCommitment() * unusedPercentageFor(loan) *              duration(loan) * riskFactorFor(loan);    }    public abstract double riskAmountFor(Loan loan);    protected double unusedPercentageFor(Loan loan) {  // hook method       return 1.0    }; 

Because this hook method returns 1.0, it has no effect on calculations unless the method is overridden, as it is by CapitalStrategyAdvisedLine:

 public class CapitalStrategyAdvisedLine...    protected double unusedPercentageFor(Loan loan) {       return loan.getUnusedPercentage();    }; 

The hook method allows CapitalStrategyTermLoan to inherit its capital(…) calculation, rather than implement a riskAmount(…) method:

 public class CapitalStrategyTermLoan...      public double capital(Loan loan) {         return loan.getCommitment() * duration(loan) * riskFactorFor(loan);      }    protected double duration(Loan loan) {       return weightedAverageDuration(loan);    }    private double weightedAverageDuration(Loan loan)... 

So that is another way to produce a Template Method for the capital() calculation. However, it suffers from a few downsides:

  • The resulting code poorly communicates the risk-adjusted capital formula (Risk Amount x Duration x Risk Factor).

  • Two of the three CapitalStrategy subclasses, CapitalStrategyTermLoan and, as we'll see, CapitalStrategyRevolver, inherit the hook method's do-nothing behavior, which, because it is a unique step in CapitalStrategyAdvisedLine, really belongs exclusively in that class.

Now let's see how CapitalStrategyRevolver would take advantage of the new capital() Template Method. Its original capital() method looks like this:

 public class CapitalStrategyRevolver...    public double capital(Loan loan) {       return (loan.outstandingRiskAmount() * duration(loan) * riskFactorFor(loan))          + (loan.unusedRiskAmount() * duration(loan) * unusedRiskFactor(loan));    } 

The first half of the formula resembles the general formula, Risk Amount x Duration x Risk Factor. The second half of the formula is similar, but it's dealing with the unused portion of a loan. We can refactor this code to take advantage of the Template Method as follows:

 public class CapitalStrategyRevolver...    public double capital(Loan loan) {       return           super.capital(loan)          + (loan.unusedRiskAmount() * duration(loan) * unusedRiskFactor(loan));    }     protected double riskAmountFor(Loan loan) {        return loan.outstandingRiskAmount();     } 

You could argue whether this new implmentation is easier to understand than the previous one. Certainly some duplication in the formula has been removed. Yet is the resulting formula easier to follow? I think so, because it communicates that capital is calculated according to the general formula with the addition of unused capital. The addition of unused capital can be made clearer by applying Extract Method [F] on capital():

 public class CapitalStrategyRevolver...    public double capital(Loan loan) {       return super.capital(loan)  + unusedCapital(loan);    }     public double unusedCapital(Loan loan) {        return loan.unusedRiskAmount() * duration(loan) * unusedRiskFactor(loan);     } 

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