Move Accumulation to Collecting Parameter

Prev don't be afraid of buying books Next

Move Accumulation to Collecting Parameter

You have a single bulky method that accumulates information to a local variable.



Accumulate results to a Collecting Parameter that gets passed to extracted methods.





Motivation

Kent Beck defined the Collecting Parameter pattern in his classic book, Smalltalk Best Practice Patterns. A Collecting Parameter is an object that you pass to methods in order to collect information from those methods. This pattern is often coupled with Composed Method [Beck, SBPP] (see the refactoring Compose Method, 123).

To decompose a bulky method into a Composed Method, you often need to decide how to accumulate information from the methods called by the Composed Method. Instead of having each of the methods return a result, which you later combine into a final result, you can incrementally accumulate a result by passing a Collecting Parameter to each of the methods. The methods write their information to the Collecting Parameter, which accumulates all of the results.

A Collecting Parameter may also be passed to methods on multiple objects. When it visits multiple objects, a Collecting Parameter accumulates information in one of two ways. Either each object calls back on a method or methods on the Collecting Parameter to pass data to it, or the objects pass themselves to the Collecting Parameter, which then calls back on the objects to obtain their data.

Collecting Parameters are programmed to accumulate data from specific classes with specific interfaces. They don't work so well when they must accumulate data from classes that hold diverse data and have diverse interfaces for accessing that data. For that case, a Visitor may be a better approach (see Move Accumulation to Visitor, 320).

Collecting Parameter works nicely with the Composite [DP] pattern because you can use a Collecting Parameter to recursively accumulate information from a Composite structure. The JUnit framework by Kent Beck and Erich Gamma uses a Collecting Parameter named TestResult to accumulate test result information from every test in a Composite of test cases.

I combined Collecting Parameter with Composite when I refactored a class's toString() method. A profiler had shown that the string concatenation in toString() was slow. (This happened before compiler makers had made string concatenation just as fast as using a StringBuffer.) So my initial goal was to replace a lot of slow string concatenation code with faster StringBuffer code, but when I realized that a simple replacement would generate lots of StringBuffer instances (because the code is recursive), I retreated from this approach. Then my programming partner at the time, Don Roberts, seized the keyboard, saying "I've got it, I've got it!" He quickly refactored the code to use a single StringBuffer as a Collecting Parameter. The resulting code had a simpler design that communicated better with the reader.

Benefits and Liabilities

+

Helps transform bulky methods into smaller, simpler, z methods.

+

Can make resulting code run faster.







Mechanics

1. Identify an accumulation method, a method that accumulates information into a result. The result, a local variable, will become a Collecting Parameter. If the result's type won't let you iteratively gather data across methods, change its type. For example, Java's String won't let you accumulate results across methods, so use a StringBuffer (see the Example section for more on this).

  • Compile.

2. In the accumulation method, find an information accumulation step and apply Extract Method [F] to extract it into a private method. Make sure the method's return type is void, and pass the result to it as a parameter. Inside the extracted method, write information to the result.

  • Compile and test.

3. Repeat step 2 for every accumulation step, until the original code has been replaced with calls to extracted methods that accept and write to the result. The accumulation method should now contain three lines of code that

  • Instantiate a result.

  • Pass a result to the first of many methods.

  • Obtain a result's collected information.

  • Compile and test.

By applying steps 2 and 3, you will be applying Compose Method (123) on the accumulation method and the various extracted methods you produce.

Example

In this example, I'll show you how to refactor Composite-based code to use a Collecting Parameter. I'll start with a Composite that can model an XML tree (see Replace Implicit Tree with Composite, 178 for a complete example).

The Composite is modeled with a single class, called TagNode, which has a toString() method. The toString() method recursively walks the nodes in an XML tree and produces a final String representation of what it finds. It does a fair amount of work in 11 lines of code. In the steps presented here, I refactor toString() to make it simpler and easier to understand.

1. The following toString() method recursively accumulates information from every tag in a Composite structure and stores results in a variable called result:

 class TagNode...    public String toString() {       String result = new String();       result += "<" + tagName + " " + attributes + ">";       Iterator it = children.iterator();       while (it.hasNext()) {          TagNode node = (TagNode)it.next();          result += node.toString();       }       if (!value.equals(""))          result += value;       result += "</" + tagName + ">";       return result;    } 

I change result's type to be a StringBuffer:

  StringBuffer result = new StringBuffer(""); 

The compiler is happy with this change.

2. I identify the first information accumulation step: code that concatenates an XML open tag along with any attributes to the result variable. I apply Extract Method [F] on this code as follows, so that this line:

 result += "<" + tagName + " " + attributes + ">"; 

is extracted to:

  private void writeOpenTagTo(StringBuffer result) {    result.append("<");    result.append(name);    result.append(attributes.toString());    result.append(">");  } 

The original code now looks like this:

  StringBuffer result = new StringBuffer("");  writeOpenTagTo(result); ... 

I compile and test to confirm that everything is OK.

3. Next, I want to continue applying Extract Method [F] on parts of the toString() method. I focus on the code that adds child XML nodes to result. This code contains a recursive step (highlighted in bold):

 class TagNode...    public String toString()...       Iterator it = children.iterator();       while (it.hasNext()) {          TagNode node = (TagNode)it.next();          result +=  node.toString();       }       if (!value.equals(""))          result += value;       ...    } 

The recursive step means that the Collecting Parameter needs to be passed to the toString() method. But that's a problem, as the following code shows:

 private void writeChildrenTo(StringBuffer result) {    Iterator it = children.iterator();    while (it.hasNext()) {       TagNode node = (TagNode)it.next();        node.toString(result); // can't do this because toString() doesn't take arguments.    }    ... } 

Because toString() doesn't take a StringBuffer as an argument, I can't simply extract the method. I have to find another solution. I decide to solve the problem using a helper method, which will do the work that toString() used to do but will take a StringBuffer as a Collecting Parameter:

 public String toString() {     StringBuffer result = new StringBuffer("");     appendContentsTo(result);     return result.toString(); }  private void appendContentsTo(StringBuffer result) {     writeOpenTagTo(result);     ...  } 

Now the recursion that's needed can be handled by the appendContentsTo() method:

 private String appendContentsTo(StringBuffer result) {    writeOpenTagTo(result);     writeChildrenTo(result);     ...    return result.toString(); } private void writeChildrenTo(StringBuffer result) {    Iterator it = children.iterator();    while (it.hasNext()) {       TagNode node = (TagNode)it.next();        node.appendContentsTo(result);  // now recursive call will work    }    if (!value.equals(""))        result.append(value); } 

As I stare at the writeChildrenTo() method, I realize that it is handling two steps: adding children recursively and adding a value to a tag, when one exists. To make these two separate steps stand out, I extract the code for handling a value into its own method:

  private void writeValueTo(StringBuffer result) {     if (!value.equals(""))        result.append(value);  } 

To finish the refactoring, I extract one more method that writes an XML close tag. Here's how the final code looks:

 public class TagNode ...    public String toString() {        StringBuffer result = new StringBuffer("");        appendContentsTo(result);        return result.toString();    }    private void appendContentsTo(StringBuffer result) {       writeOpenTagTo(result);       writeChildrenTo(result);       writeValueTo(result);       writeEndTagTo(result);    }    private void writeOpenTagTo(StringBuffer result) {       result.append("<");       result.append(name);       result.append(attributes.toString());       result.append(">");    }    private void writeChildrenTo(StringBuffer result) {       Iterator it = children.iterator();       while (it.hasNext()) {          TagNode node = (TagNode)it.next();          node.appendContentsTo(result);       }    }    private void writeValueTo(StringBuffer result) {       if (!value.equals(""))          result.append(value);    }    private void writeEndTagTo(StringBuffer result) {       result.append("</");       result.append(name);       result.append(">");    } } 

I compile, run my tests, and everything is good. The toString() method is now very simple, while the appendContentsTo() method is a fine example of a Composed Method (see Compose Method, 123).

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