Some people purport that you can keep the "cost of change" curve relatively flat. In other words, you can introduce unplanned features any time in the future as cheaply as if you had inserted them today. That's a bold statement. I agree with itif and only if you refactor with extreme vigilance.
To be successful, you will need to become a refactoring zealot. This means you learn to understand what duplication really means, and ferret it out at every possible opportunity. Every TDD cycle includes writing test code, writing production code, then refactoring. Any new code introduced in the first two parts of the cycle must not degrade the system's current clean design. Otherwise you cannot claim that the cycle is complete.
With the 10-minute bit of code I wrote to support saving and retrieving users, I've already spotted gobs of duplication and expressiveness problems. As I refactor more, I seem to spot even more. It's like clearing the underbrush beneath trees in your yard. Picking up the larger branches on top alerts you to the presence of the smaller branches snaking through the grass. They too have to be cleaned up if you want to keep your grass looking pristine.
Yesterday I mentioned my desire to use static import. That was my first subject of refactoring today. I moved all of the classes into appropriate packages. I even renamed a few classes (plus one method), and at least one class I renamed more than once! I initially moved JdbcAccess into a package called jdbc, then renamed it to Access (to remove the redundancy between the class name and the package). But then I figured I'd be better off with the generic package name persistence, so I renamed the class back to JdbcAccess. A bit of unfortunate thrashing, indeed, but it was a cheap exercise. The beauty of Eclipse and other IDEs is that you don't have to have a perfect name first. You can continually improve identifier names as you discover more about what they represent. Very powerful! This makes the third simple design rule (code should be expressive, basically) easy and even fun to tackle.
The new project organization:
So now, I could use the static import facility:
package util;
import junit.framework.*;
import static util.StringUtil.commaDelimit;
public class StringUtilCommaDelimitTest extends TestCase {
public void testDegenerate() {
assertEquals("", commaDelimit(new String[] {}));
}
public void testOneEntry() {
assertEquals("a", commaDelimit(new String[] {"a"}));
}
public void testTwoEntries() {
assertEquals("a,b", commaDelimit(new String[] {"a", "b"}));
}
public void testEmbeddedCommas() {
// not very well defined yet! don't care so far.
assertEquals("a,,,b", commaDelimit(new String[] {"a,", ",b"}));
}
}
I think this is a perfect use of the (relatively) new J2SE 5.0 feature. Eclipse italicizes the method call, so it's clear that it's a static method.
The next part of today's work (I was only up to about 3 minutes worth of refactoring at this point) was eliminating the duplication inherent in putting together a value list for the insert statement. Step one was to do an extract method refactoring to make it clear what was getting cleaned up. In User:
...
public void save() {
new JdbcAccess().execute("insert into " + TABLE_NAME + " ("
+ User.createColumnList() + ") values (" +
createValuesList() + ")");
}
private String createValuesList() {
return String.format("'%s', '%s'", name, password);
}
...
Step two: enhance my string utility method, looking for duplication along the way. There was lots of it. Here's what I ended up with:
public void save() {
new JdbcAccess().execute("insert into " + TABLE_NAME + " ("
+ User.createColumnList() + ") values (" +
createValuesList() + ")");
}
private String createValuesList() {
Transformer ticWrapper = new Transformer() {
public String transform(String input) {
return StringUtil.wrap(input, '\'');
}};
return StringUtil.commaDelimit(new String[] { name, password }, ticWrapper);
}
private static String createColumnList() {
return StringUtil.commaDelimit(columns);
}
package util;
import junit.framework.*;
import static util.StringUtil.commaDelimit;
public class StringUtilCommaDelimitTest extends TestCase {
...
public void testCommaDelimitWithCallback() {
Transformer duplicator = new Transformer() {
public String transform(String input) {
return input + input;
}};
assertEquals("aa,bb", commaDelimit(new String[] {"a", "b"}, duplicator));
}
}
package util;
public interface Transformer {
String transform(String input);
}
package util;
import junit.framework.*;
import static util.StringUtil.wrap;
public class StringUtilWrapTest extends TestCase {
public void testDegenerate() {
assertNull(wrap(null, '\''));
}
public void testSingleCharacterString() {
assertEquals("*a*", wrap("a", '*'));
}
public void testMultipleCharacterString() {
assertEquals("-abc-", StringUtil.wrap("abc", '-'));
}
}
package util;
public class StringUtil {
public static String commaDelimit(String[] strings, Transformer transformer) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < strings.length; i++) {
if (i > 0)
builder.append(',');
builder.append(transformer.transform(strings[i]));
}
return builder.toString();
}
public static String commaDelimit(String[] strings) {
final Transformer nullTransform = new Transformer() {
public String transform(String input) {
return input;
}
};
return commaDelimit(strings, nullTransform);
}
public static String wrap(String source, char wrapper) {
if (source == null)
return null;
StringBuilder builder = new StringBuilder();
builder.append(wrapper);
builder.append(source);
builder.append(wrapper);
return builder.toString();
}
}
Interestingly, eliminating duplication has meant more code up to this point.
Also, it's inevitable that you'll have duplication at some level. Here it exists in the wrap method. I've chosen to foist the duplication from a volatile area (code in User) to the most abstract level possible, this wrap method. I don't see this method as ever having to change.
There's still SQL-related code in the domain class. That might be my next step, unless someone has another thought.
February 2004 March 2004 May 2004 September 2004 October 2004 January 2005 February 2005 September 2005 October 2005 November 2005 December 2005 January 2006 February 2006 March 2006 June 2006 August 2006 January 2007 February 2007 March 2007 April 2007 September 2007 October 2007 November 2007 December 2007 January 2008