OK, I lied. I'm not tackling building an abstraction for the metadata yet. I'd rather hit duplication first and see what that leads me to.
Concerned about how long I might take to get to a green bar, I put the timer on.
Goal: factor the duplicate save methods into a common
representation. The steps in mind:
save methodsave method, construct a persister with the metadatasave method on the persister, passing the object to be persisted
package domain;
import java.util.*;
import persistence.*;
public class UserAccess implements PersistableMetadata {
private static final String TABLE = "userdata";
private static String[] COLUMNS = { User.NAME, User.PASSWORD };
public void save(Persistable persistable) {
new Persister(this).save(persistable);
}
public User find(String nameKey) {
...
}
public String getTable() {
return TABLE;
}
public String[] getColumns() {
return COLUMNS;
}
}
package domain;
public interface PersistableMetadata {
String getTable();
String[] getColumns();
}
package domain;
import persistence.*;
public class Persister {
private PersistableMetadata metadata;
public Persister(PersistableMetadata metadata) {
this.metadata = metadata;
}
public void save(Persistable persistable) {
String[] columns = metadata.getColumns();
String[] fields = new String[columns.length];
for (int i = 0; i < columns.length; i++)
fields[i] = persistable.get(columns[i]);
String sql = new SqlGenerator().createInsert(metadata.getTable(), columns, fields);
new JdbcAccess().execute(sql);
}
}
Pretty simple. Checking the timer, I'm at a little over 4 minutes, and I have a green bar.
Even though my tests pass, I try to never extract a new class without adding some level of test to it. If I can't get a reasonable test against the new class, then it suggests something's awry with my refactoring. Timer on.
package domain;
import persistence.*;
import junit.framework.*;
public class PersisterTest extends TestCase {
private static final String TABLE = "x";
private static final String[] COLUMNS = { "a", "b" };
private String lastSql;
public void testSave() {
PersistableMetadata metadata = new PersistableMetadata() {
public String getTable() {
return TABLE;
}
public String[] getColumns() {
return COLUMNS;
}
};
Persistable persistable = new Persistable() {
public String get(String key) {
if (key.equals(COLUMNS[0]))
return "0";
if (key.equals(COLUMNS[1]))
return "1";
return null;
}
};
JdbcAccess access = new JdbcAccess() {
public void execute(String sql) {
lastSql = sql;
}
};
Persister persister = new Persister(metadata, access);
persister.save(persistable);
assertEquals("insert into x (a,b) values ('0','1')", lastSql);
}
}
Gak. Three anonymous inner class overrides. Who has a problem with that? I've come across
many developers who just hate anonymous inner classes. Me, I tend to use them all the time
when I need to stub something for purposes of a test.
Here, the metadata and persistable
dynamic class definitions do something slightly different: they define an invariant class
that I can guarantee for purposes of the test. They are no different than any other class
I might define. The JdbcAccess override, however, dummies out the execute
method, so that I can (a) avoid having to do actual, slow persistence when running the
test and (b) prove that Persister is working by testing against the SQL string that
would otherwise be executed.
Using the JdbcAccess override does require changes to the Persister class itself. I had to add an additional constructor, and then do something similar to Parameterize Constructor (see Michael Feathers' book Working Effectively With Legacy Code).
package domain;
import persistence.*;
public class Persister {
private PersistableMetadata metadata;
private JdbcAccess access;
// general use production constructor
public Persister(PersistableMetadata metadata) {
this(metadata, new JdbcAccess());
}
// new overloaded constructor
public Persister(PersistableMetadata metadata, JdbcAccess access) {
this.metadata = metadata;
this.access = access;
}
public void save(Persistable persistable) {
String[] columns = metadata.getColumns();
String[] fields = new String[columns.length];
for (int i = 0; i < columns.length; i++)
fields[i] = persistable.get(columns[i]);
String sql = new SqlGenerator().createInsert(metadata.getTable(), columns, fields);
access.execute(sql); // no longer directly instantiating here
}
}
Timer check: about 5 minutes (typing fast). Final step for today: refactor CustomerAccess the same way. Timer check: about 45 seconds.
Next: there's now some obvious duplication in CustomerAccess and UserAccess.
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