The Introduce Parameter Object refactoring is one of my favorite refactorings as of late. A method with a fair number of parameters is often a sign of a new class needing to be extracted. What's a class but a collection of data and methods to act on it? All of the parameters could potentially be members of a new class.
What's interesting is that the IPO refactoring itself usually doesn't yield anything more than an increase in the verbosity of the code and a new class that's just data without any methods. However, if some follow through is applied, the new parameter object can become a first class class (ahem).
Given a fake implementation like this (stealing slightly from a code sample posted to the pragprog mailing list):
package foo;
import java.util.ArrayList;
import java.util.List;
public class Foo {
private List<String> bars = new ArrayList<String>();
public void generateMumble(int unlimitedIterations, int start, int end, boolean aggressivePruning) {
int count = 0;
for (int i = start; i <= end; i++) {
if (i > (bars.size() - 1)) { return; }
String bar = bars.get(i);
if (aggressivePruning) {
tryHardToPruneThisBar(bar);
}
count++;
if (unlimitedIterations > -1) {
if (count > unlimitedIterations) { return; }
}
}
}
private void tryHardToPruneThisBar(String bar) {
}
}
and a test like this:
package foo;
import junit.framework.TestCase;
public class FooTest extends TestCase {
public void testMumble() {
Foo foo = new Foo();
foo.generateMumble(-1, 1, 42, true);
}
}
Running the introduce parameter object refactoring in Eclipse (with setters) gives:
package foo;
import java.util.ArrayList;
import java.util.List;
public class Foo {
private List<String> bars = new ArrayList<String>();
public static class GenerateMumbleParameters {
public int unlimitedIterations;
public int start;
public int end;
public boolean aggressivePruning;
public GenerateMumbleParameters(int unlimitedIterations, int start,
int end, boolean aggressivePruning) {
this.unlimitedIterations = unlimitedIterations;
this.start = start;
this.end = end;
this.aggressivePruning = aggressivePruning;
}
public void setUnlimitedIterations(int unlimitedIterations) {
this.unlimitedIterations = unlimitedIterations;
}
public void setStart(int start) {
this.start = start;
}
public void setEnd(int end) {
this.end = end;
}
public void setAggressivePruning(boolean aggressivePruning) {
this.aggressivePruning = aggressivePruning;
}
}
public void generateMumble(GenerateMumbleParameters parameterObject) {
int count = 0;
for (int i = parameterObject.start; i <= parameterObject.end; i++) {
if (i > (bars.size() - 1)) { return; }
String bar = bars.get(i);
if (parameterObject.aggressivePruning) {
tryHardToPruneThisBar(bar);
}
count++;
if (parameterObject.unlimitedIterations > -1) {
if (count > parameterObject.unlimitedIterations) { return; }
}
}
}
private void tryHardToPruneThisBar(String bar) {
}
}
package foo;
import foo.Foo.GenerateMumbleParameters;
import junit.framework.TestCase;
public class FooTest extends TestCase {
public void testMumble() {
Foo foo = new Foo();
foo.generateMumble(new GenerateMumbleParameters(-1, 1, 42, true));
}
}
This isn't that much better (in fact it's worse), but we can now transform the constructor to use reasonable defaults (this is not always possible) and use setters instead of passing the params in the constructor:
public static class GenerateMumbleParameters {
public int unlimitedIterations;
public int start;
public int end;
public boolean aggressivePruning;
public GenerateMumbleParameters() {
this.unlimitedIterations = -1;
this.start = 0;
this.end = 0;
this.aggressivePruning = false;
}
public void setUnlimitedIterations(int unlimitedIterations) {
this.unlimitedIterations = unlimitedIterations;
}
public void setStart(int start) {
this.start = start;
}
public void setEnd(int end) {
this.end = end;
}
public void setAggressivePruning(boolean aggressivePruning) {
this.aggressivePruning = aggressivePruning;
}
}
package foo;
import foo.Foo.GenerateMumbleParameters;
import junit.framework.TestCase;
public class FooTest extends TestCase {
public void testMumble() {
Foo foo = new Foo();
GenerateMumbleParameters generateMumbleParameters = new GenerateMumbleParameters();
generateMumbleParameters.setUnlimitedIterations(-1);
generateMumbleParameters.setStart(1);
generateMumbleParameters.setEnd(42);
generateMumbleParameters.setAggressivePruning(true);
foo.generateMumble(generateMumbleParameters);
}
}
Now at least the calling code is more readable, and in fact the unlimited iterations line isn't even necessary, since we're happy with the default.
So, is this worth it? It seems a high cost of this additional parameter class which is merely data - no methods for increased readability in the calling code. But a smell has appeared in our original method:
public void generateMumble(GenerateMumbleParameters parameterObject) {
int count = 0;
for (int i = parameterObject.start; i <= parameterObject.end; i++) {
if (i > (bars.size() - 1)) { return; }
String bar = bars.get(i);
if (parameterObject.aggressivePruning) {
tryHardToPruneThisBar(bar);
}
count++;
if (parameterObject.unlimitedIterations > -1) {
if (count > parameterObject.unlimitedIterations) { return; }
}
}
}
Feature Envy.
So, let's push this functionality into our new class. Doing a Move refactoring in Eclipse by default assumes this method should be moved into the new Parameter class:
package foo;
import java.util.ArrayList;
import java.util.List;
public class Foo {
private List<String> bars = new ArrayList<String>();
public static class GenerateMumbleParameters {
public int unlimitedIterations;
public int start;
public int end;
public boolean aggressivePruning;
public GenerateMumbleParameters() {
this.unlimitedIterations = -1;
this.start = 0;
this.end = 0;
this.aggressivePruning = false;
}
public void setUnlimitedIterations(int unlimitedIterations) {
this.unlimitedIterations = unlimitedIterations;
}
public void setStart(int start) {
this.start = start;
}
public void setEnd(int end) {
this.end = end;
}
public void setAggressivePruning(boolean aggressivePruning) {
this.aggressivePruning = aggressivePruning;
}
public void generateMumble(Foo foo) {
int count = 0;
for (int i = start; i <= end; i++) {
if (i > (foo.bars.size() - 1)) { return; }
String bar = foo.bars.get(i);
if (aggressivePruning) {
foo.tryHardToPruneThisBar(bar);
}
count++;
if (unlimitedIterations > -1) {
if (count > unlimitedIterations) { return; }
}
}
}
}
private void tryHardToPruneThisBar(String bar) {
}
}
package foo;
import foo.Foo.GenerateMumbleParameters;
import junit.framework.TestCase;
public class FooTest extends TestCase {
public void testMumble() {
Foo foo = new Foo();
GenerateMumbleParameters generateMumbleParameters = new GenerateMumbleParameters();
generateMumbleParameters.setUnlimitedIterations(-1);
generateMumbleParameters.setStart(1);
generateMumbleParameters.setEnd(42);
generateMumbleParameters.setAggressivePruning(true);
generateMumbleParameters.generateMumble(foo);
}
}
Our parameter object is no longer just a parameter object, so let's rename:
package foo;
import java.util.ArrayList;
import java.util.List;
public class Foo {
private List<String> bars = new ArrayList<String>();
public static class MumbleGenerator {
public int unlimitedIterations;
public int start;
public int end;
public boolean aggressivePruning;
public MumbleGenerator() {
this.unlimitedIterations = -1;
this.start = 0;
this.end = 0;
this.aggressivePruning = false;
}
public void setUnlimitedIterations(int unlimitedIterations) {
this.unlimitedIterations = unlimitedIterations;
}
public void setStart(int start) {
this.start = start;
}
public void setEnd(int end) {
this.end = end;
}
public void setAggressivePruning(boolean aggressivePruning) {
this.aggressivePruning = aggressivePruning;
}
public void execute(Foo foo) {
int count = 0;
for (int i = start; i <= end; i++) {
if (i > (foo.bars.size() - 1)) { return; }
String bar = foo.bars.get(i);
if (aggressivePruning) {
foo.tryHardToPruneThisBar(bar);
}
count++;
if (unlimitedIterations > -1) {
if (count > unlimitedIterations) { return; }
}
}
}
}
private void tryHardToPruneThisBar(String bar) {
}
}
package foo;
import foo.Foo.MumbleGenerator;
import junit.framework.TestCase;
public class FooTest extends TestCase {
public void testMumble() {
Foo foo = new Foo();
MumbleGenerator mumbler = new MumbleGenerator();
mumbler.setUnlimitedIterations(-1);
mumbler.setStart(1);
mumbler.setEnd(42);
mumbler.setAggressivePruning(true);
mumbler.execute(foo);
}
}
I'd probably go one more step and pass the foo instance into the constructor of MumbleGenerator instead of the execute method.
At this point, in this contrived example, there's not much point in having a Foo class anymore ... but hopefully you get the point I'm trying to make. Introduce parameter object is the first step in turning a method in class A inside out and producing a new class B.
tags: ComputersAndTechnology