TDD

さて、暇でもないんだけど、暇つぶしに前に説明したCompositeパターンのSelectクラスを実装してみた。
そんなに難しいクラスでもないけど、
ブログに載せるつもりで書いてたので、途中経過を取りながら作った。

実践TDDといったところだろうか。
デザパタとは違うけど、それに類する技術なので必見だ。
(↑自分で言うなっつーの!)
次回は新しいデザパタを書く。 2度あることは・・・ではないと思う(笑)

では、順を追って説明する。TDDの詳しい説明はしないが、
テストファーストというくらいなのでまずはテストコードだ。


public void testSelectBasic() {
Select select = new Select("table");
assertEquals("select * from table", select.create());
}
}

まずはベーシックなselect文を作るテストコードだ。
KISSに則り最低限のコードでこれを実現する。

public class Select {
private String _table;
public Select(String table) {
_table = table;
}
public String create() {
return "select * from " + _table;
}
}

よし。 コンパイルOK。テストOK。

では次。
select *では味気ないので、
フィールド指定ができるようにしてみる。

public void testSelectSetField() {
Select select = new Select("table");
select.addColumn("column1");
assertEquals("select column1 from table", select.create());
}

これが次のテスト。
では(ry

public class Select {
private String _table;
private String _column;
public Select(String table) {
_table = table;
}
public String create() {
String column = "*";
if (null != _column) {
column = _column;
}
return "select " + column + " from " + _table;
}
public void addColumn(String column) {
_column = column;
}
}

さて、これが最低限だろう。
見て分かるとおり、addと言いながらも上書きしている。
これを明らかにするためにテストコードを追加する。

public void testAddFields() {
select = new Select("table");
select.addColumn("column1");
select.addColumn("column2");
select.addColumn("column3");
assertEquals("select column1, column2, column3 from table",
select.create());
}

テストをすれば一目瞭然でRedになる。
ではこれをGreenにしよう。

public class Select {
private String _table;
private List _columns = new ArrayList(0);
public Select(String table) {
_table = table;
}
public String create() {
return "select " + getColumn() + " from " + _table;
}
public void addColumn(String column) {
_columns.add(column);
}
private String getColumn() {

String columns = "";
for (int i = 0; i < _columns.size(); i++) {
if (i > 0) {
columns += ", ";
}
columns += _columns.get(i);
}

if ("".equals(columns)) {
return "*";
}

return columns;
}
}

Listでcolumnを管理するようにした。
Java5のGenericsを使っているのでそこだけ注意だ。
これでフィールドは完璧だろう。

では、次にwhereの実装だ。
・・・といきたいところだが、テストコードを見て欲しい。

public class SelectTest extends TestCase {
public void testBasic() {
Select select = new Select("table");
assertEquals("select * from table", select.create());
}
public void testAddField() {
Select select = new Select("table");
select.addColumn("column1");
assertEquals("select column1 from table", select.create());
}
public void testAddFields() {
Select select = new Select("table");
select.addColumn("column1");
select.addColumn("column2");
select.addColumn("column3");
assertEquals("select column1, column2, column3 from table",
select.create());
}
}

Select select = new Select("table");
が全てのメソッドに書かれている。非常にダサいので
setUp()メソッドに書くことにする。

public class SelectTest extends TestCase {
private Select _select;
protected void setUp() throws Exception {
_select = new Select("table");
}
public void testBasic() {
assertEquals("select * from table", _select.create());
}
public void testAddField() {
_select.addColumn("column1");
assertEquals("select column1 from table", _select.create());
}
public void testAddFields() {
_select.addColumn("column1");
_select.addColumn("column2");
_select.addColumn("column3");
assertEquals("select column1, column2, column3 from table",
_select.create());
}
}

よし。スマートになった。
コードが正しく動くか、もう一度テストだ。
OK。AllGreen。 順調だ。

ではwhereの実装だ。
例のごとくテストコードだ。

public void testSetWhere() {
_select.setWhere("column1", "10");
assertEquals("select * from table where column1 = '10'",
_select.create());
}

初めはこれでいいだろう。
では実装してみる。

private String _where = "";

public String create() {
return "select " + getColumn() + " from " + _table + _where;
}
public void setWhere(String column, String value) {
_where = " where " + column + " = '" + value + "'";
}

変更点だけ載せているので注意。
これもcolumnと同じく複数には対応していないので
複数の条件があるテストを作ってそれを明らかにする。

public void testAddWheres() {
_select.addWhere("column1", "10");
_select.addWhere("column2", "20");
assertEquals("select * from table where column1 = '10' and column2 = '20'",
_select.create());
}

またaddに変更した。これで行ってみる。

public void addWhere(String column, String value) {
if ("".equals(_where)) {
_where = " where " + column + " = '" + value + "'";
} else {
_where += " and " + column + " = '" + value + "'";
}
}

これでOKだろう。
テストはGreen。
だが、見て分かるとおり、andが決め打ちだ。
これはまずい。 orをこの後に追加できるように拡張しよう。

public void testAddWheresOr() {
_select.addWhere("column1", "10");
_select.addWhere("column2", "20");
_select.addWhereOr("column3", "30");
assertEquals("select * from table where column1 = '10' and column2 = '20' or column3 = '30'",
_select.create());
}

テストコードはこうなった。
実装に入る・・・ 前に良く見て欲しい。
whereの追加で複数のメソッドができた。 これはよしとしよう。
だが、このままいけば、Selectクラスのコードのほとんどがwhereの処理になってしまう。
これは非常に嫌な臭いだ。

とりあえず、whereはカラム=コードとなっているのでそこのところをクラス化してみよう。

public void testAddWhere() {
_select.addWhere(new Equal("column1", "10"));
assertEquals("select * from table where column1 = '10'",
_select.create());
}

public void testAddWheres() {
_select.addWhere(new Equal("column1", "10"));
_select.addWhere(new Equal("column2", "20"));
assertEquals("select * from table where column1 = '10' and column2 = '20'",
_select.create());
}

public void testAddWheresOr() {
_select.addWhere("column1", "10");
_select.addWhere("column2", "20");
_select.addWhereOr("column3", "30");
assertEquals("select * from table where column1 = '10' and column2 = '20' or column3 = '30'",
_select.create());
}

Equalクラスを作ってみる。(テストコード上でなので本体はまだ無い。)
これで解決すればいいのだが・・と思いつつ書き換えてみると・・・
ほとんど変わらない。 testAddWheresOrを書き換える前にこれではダメなことに気付くだろう。 もう少し深く考えてみよう。
=をクラス化してダメって事は、結合子のandやorをクラスにすることになる。 (結構無理やりだが・・・)

andもorもEqualも同じwhereの仲間で、かつandやorはEqualを包括していることよりCompositeパターンが使えそうだと分かるだろう。
そもそも and や orというのはツリー構造になっているということに気付いていれば、Compositeパターンに行き着くのはそう難しいことではない。

では、半ば無理やりではあるが、Compositeパターンを適用する。
AndクラスやOrクラスを作った。
Andクラスのテストの一部を載せておく。

public void testBasic() {
Where where = new And(new Equal("column1", "10"), new Equal("column2", "20"));
assertEquals("(column1 = '10' and column2 = '20')", where.get());
}

AndとOrは実装にほとんど差が無いので、ここではAndのみ。

public class And implements Where {
Where _left;
Where _right;
public And(Where left, Where right) {
_left = left;
_right = right;
}
public String get() {
return "(" + _left.get() + " and " + _right.get() + ")";
}
}

こんな感じだ。
WhereインターフェイスはEqualクラスも実装しているインターフェイスだ。
get()メソッドのみを持っている。

では、最後の仕上げだ。
SelectにはWhere型のフィールドが一つあればいい事から、
setWhere()があれば十分実用に足るだろう。
そして、何も設定しない場合〜というのをSelectに書くのはエレガントではないので、何も無い場合を表すためにNullObjectパターンを適用する。

public interface Where {
String get();

public static Where NULL = new Where() {
public String get() {
return "";
};
};
}

初めはSelectクラスに書いたのだが、Whereにあるべきかと思い移動した。
これを初期化時に設定しておく。

テストしてみると、"where"がない。

if (Where.NULL == _where) {
return "";
} else {
return " where " + _where.get();
}

と書けば問題は解決するのだが、せっかくNullObjectを導入したのにNULLかどうか判断するコードを書いているのは導入の意味が無さすぎるので、
知恵を絞って
Rootクラスを作った。

class Root implements Where {
private Where _where;
Root(Where where) {
_where = where;
}
public String get() {
return " where " + _where.get();
}
}

ユーザが使うことは無いので、パッケージプライベートにして外部アクセスは付加にしていた方がいいだろう。
これを組み合わせて、完成した。


ソースコード(テスト含む)とクラス図を一緒にしたファイルは↓
http://pluswing.net/download/Select.zip
↓同梱してあるがこれが完成したクラス図。
Selectクラス図