[brownies] ResultSetFilter

前回のTableUtilsではクラスの中味についてはほとんど記述しませんでしたが、今回は実装を中心に説明します。このクラスもResultSetがインタフェースとして定義されている点を生かしてDecoratorパターンを使用しています。

業務アプリなどでたまに見かけるのが、こんなコードです。

public void testToDefaultStringResultSetString() {
    final List beans = new ArrayList();
    try {
        //statementはsetupで生成されています。
        final ResultSet resultSet = statement.executeQuery("select a, b, c from test1");
        try {
            String val;
            while (resultSet.next()) {
                final Test1 bean = new Test1();
                val = resultSet.getString(0);
                if (val == null)
                    bean.setA("");
                else
                    bean.setA(val);
                val = resultSet.getString(1);
                if (val == null)
                    bean.setB("");
                else
                    bean.setB(val);
                val = resultSet.getString(2);
                if (val == null)
                    bean.setC("");
                else
                    bean.setC(val);
                beans.add(bean);
            }
        } finally {
            resultSet.close();
        }
    } catch (SQLException e) {
        e.printStackTrace();
        fail();
    }
    assertEquals(3, beans.size());
    final Test1 test1_1 = (Test1) beans.get(0);
    assertEquals("a1", test1_1.getA());
    assertEquals("b1", test1_1.getB());
    assertEquals("c1", test1_1.getC());
    final Test1 test1_2 = (Test1) beans.get(1);
    assertEquals("a2", test1_2.getA());
    assertEquals("", test1_2.getB());
    assertEquals("", test1_2.getC());
    final Test1 test1_3 = (Test1) beans.get(2);
    assertEquals("", test1_3.getA());
    assertEquals("b3", test1_3.getB());
    assertEquals("c3", test1_3.getC());
}

これはResultSetFilterのテストケースをResultSetFilterを使わないように変更したものですが、そこは本筋じゃないので無視してください。


ResultSetのgetStringで取得していちいち変数に代入してnullかどうかを判断し、nullだったらヌル文字列をセットしているわけです。いじるのが面倒なコードですね。

結局やりたいのはnullだったらヌル文字列をプロパティにセットする、という単純なことなのです。しかし、実際に書いているのは各プロパティごとにnullかどうかを判断して・・・という余計なコードになってしまっているわけです。


ポイントは単純にResultSetがnullをヌル文字列にして返してくれればいいじゃん、と思ってみることです。具体的にはStatementが返すResultSetをいじくることはできないので、他のクラスで変換することを考えます。丁度ResultSetはインタフェースですので、そのインタフェースを実装した他のクラスを使えばいいじゃん、インタフェースを保ったままにすれば非常に可搬性の高いコードが書けるね、っていうわけでbrowniesではResultSetFilterというクラスを用意しました。まずは使い方を見てみましょう。

public void testToDefaultStringResultSetString() {
    final List beans = new ArrayList();
    try {
        //statementはsetupで生成されています。
        final ResultSet resultSet = ResultSetFilter.toDefaultString(statement
                .executeQuery("select a, b, c from test1"), "");
        try {
            String val;
            while (resultSet.next()) {
                final Test1 bean = new Test1();
                bean.setA(resultSet.getString(0));
                bean.setB(resultSet.getString(1));
                bean.setC(resultSet.getString(2));
                beans.add(bean);
            }
        } finally {
            resultSet.close();
        }
    } catch (SQLException e) {
        e.printStackTrace();
        fail();
    }
    assertEquals(3, beans.size());
    final Test1 test1_1 = (Test1) beans.get(0);
    assertEquals("a1", test1_1.getA());
    assertEquals("b1", test1_1.getB());
    assertEquals("c1", test1_1.getC());
    final Test1 test1_2 = (Test1) beans.get(1);
    assertEquals("a2", test1_2.getA());
    assertEquals("", test1_2.getB());
    assertEquals("", test1_2.getC());
    final Test1 test1_3 = (Test1) beans.get(2);
    assertEquals("", test1_3.getA());
    assertEquals("b3", test1_3.getB());
    assertEquals("c3", test1_3.getC());
}

どうです?全然読み易さが違いませんか。ちゃんとこのコードはパスします。
では実際toDefaultStringメソッドは何をしているのかというとこんな感じです。

public static ResultSet toDefaultString(ResultSet source, String defaultValue) {
    return new ResultSetDefaultStringFilter(source, defaultValue);
}

ResultSetDefaultStringFilterというクラスはResultSetFilterと同じファイルに書かれているパッケージスコープのクラスです。で、このクラスはこんなんです。

class ResultSetDefaultStringFilter extends ResultSetWrapper {
    public ResultSetDefaultStringFilter(ResultSet impl, String defaultValue) {
        super(impl);
        this.defaultValue = defaultValue;
    }

    private final String defaultValue;

    public String getString(int columnIndex) throws SQLException {
        final String result = super.getString(columnIndex);
        return (result == null) ? defaultValue : result;
    }

    public String getString(String columnName) throws SQLException {
        final String result = super.getString(columnName);
        return (result == null) ? defaultValue : result;
    }
}

getStringメソッドでnullの判断をして、nullだった場合はコンストラクタで渡されたdefaultValueを返すだけです。
継承元のResultSetWrapperというクラスは、他のResultSetに処理を委譲するだけの単純なクラスです。

public class ResultSetWrapper extends Wrapper implements ResultSet {

    public ResultSetWrapper(ResultSet impl) {
    	super(impl);
    	this.impl = impl;
    }

    protected final ResultSet impl;

    public String getString(int columnIndex) throws SQLException {
    	return impl.getString(columnIndex);
    }

    // 他のメソッドも同様にimplのメソッドを呼び出すだけですので以下略
}

こんなクラスは作りたくないのが本音なのですが、そうするとResultSetDefaultStringFilterクラスがResultSetインタフェースの全てのメソッドを実装しなければなりません。ResultSetのメソッドは大量にあるので、どのメソッドに変更を加えているのかが分かりにくくなってしまいます。処理を実装しているメソッドを際立たせるためにResultSetWrapperというクラスを作りました。
逆に言うと、このクラスさえ作っておけばアプリ依存のクラスないでResultSetに何か変更を加えたい場合に、

        final ResultSet resultSet = new ResultSetWrapper(statement
                .executeQuery("select a, b, c from test1")){
            public String getString(int columnIndex) throws SQLException {
                final String result = super.getString(columnIndex);
                return (result == null) ? "" : result;
            }
        };

というようなコードも書けるわけです。
やっぱりDecoratorパターン最高!と今回も同じ締めですみません。

次回はMapTreeあたりをご紹介します。