AndクラスとCriteriaインターフェイス
前述のAndクラスでは、2つのEqualsオブジェクトの結合のみをサポートしていました。このAndクラスを改良して、Likeオブジェクトや、これから思いつくかもしれない他の条件もサポートするようにしたいと思います。それぞれの条件がSQL表現を返す機能は、共通のインターフェイスとして抜き出すことができます。
public interface Criteria { String sqlString(); } // EqualsTest.java: import static org.junit.Assert.*; import org.junit.*; public class EqualsTest { @Test public void stringColumn() { Column column = new StringColumn("name", 10); ''Criteria'' criteria = new Equals(column, "joe"); assertEquals("name = 'joe'", criteria.sqlString()); } @Test public void numericColumn() { Column column = new NumericColumn("amount"); ''Criteria'' criteria = new Equals(column, 5); assertEquals("amount = 5", criteria.sqlString()); } } // Equals.java: public class Equals ''implements Criteria'' { private Column column; private Object value; public Equals(Column column, Object value) { this.column = column; this.value = value; } public String sqlString() { return String.format("%s = %s", column.getName(), column.sqlValue(value)); } }
Likeクラスを修正して、同じCriteriaインターフェイスを実装するようにします。そのためには、若干のリファクタリングが必要です。メソッド名の不一致があるからです。
// LikeTest.java: import static org.junit.Assert.*; import org.junit.*; public class LikeTest { @Test public void simple() { ''Criteria'' like = new Like(new StringColumn("name", 1), "Joe%"); assertEquals("name like 'Joe%'", like.''sqlString''()); } } // Like.java public class Like ''implements Criteria'' { private StringColumn column; private String value; public Like(StringColumn column, String value) { this.column = column; this.value = value; } public String ''sqlString''() { return String.format("%s like %s", column.getName(), column.sqlValue(value)); } }
LikeとEqualsの両方でCriteriaインターフェイスを実装したので、この変更内容をAndクラスに反映させます。
// AndTest.java import static org.junit.Assert.*; import org.junit.*; public class AndTest { @Test public void and() { ''Criteria'' name = new ''Like''(new StringColumn("name", 1), "''Joe%''"); ''Criteria'' amount = new Equals(new NumericColumn("amount"), 5); And and = new And(name, amount); assertEquals("''name like 'Joe%''' and amount = 5", and.sql()); } } // And.java public class And { private ''Criteria'' left; private ''Criteria'' right; public And(''Criteria'' name, ''Criteria'' amount) { this.left = name; this.right = amount; } public String sql() { return left.sqlString() + " and " + right.sqlString(); } }
複合物らしいものが全然出てこないじゃないか、とお思いの人もいるかもしれません。しかしちょっと発想を変えてみると、意外な新発見があるものです。
例えば今回の場合、SQLステートメントについて少し考えてみると、Andはそれ自体が、他の条件と組み合わせることのできる1つの条件であるということに気づきます。例えば、次のようなwhere句が必要になる場合が考えられます。
where (amount = 10 and name like 'Joe%') and department = 'Labor'
修正したAndクラスを以下に示します。
// AndTest.java: import static org.junit.Assert.*; import org.junit.*; public class AndTest { @Test public void and() { Criteria name = new Like(new StringColumn("name", 1), "Joe%"); Criteria amount = new Equals(new NumericColumn("amount"), 5); ''Criteria'' and = new And(name, amount); assertEquals("name like 'Joe%' and amount = 5", and.''sqlString''()); } } // And.java: public class And ''implements Criteria'' { private ''Criteria'' left; private ''Criteria'' right; public And(''Criteria'' name, ''Criteria'' amount) { this.left = name; this.right = amount; } public String sqlString() { return left.sqlString() + " and " + right.sqlString(); } }
これで、like句とequals句とand句を自由に結合できるようになります。
次のUMLダイアグラムを見ると、このサンプルでCompositeデザインパターンがどのように実装されているかが分かります。鍵となる関係は、AndクラスはCriteriaインターフェイスの実装であり、それと同時に、And型のオブジェクトは複数のCriteriaオブジェクトから成るという点です。クライアントコードでCriteriaオブジェクトの適切な階層を構築しておけば、1つの呼び出しで、この階層の最上位のオブジェクトにsqlStringを要求できます。
Compositeデザインパターンを使わずに同じ機能をコーディングすることもできますが、その場合は、メソッドのオーバーロードやifステートメントがもっとたくさん必要になるので、システムが複雑になります。

参考資料
- 『Design Patterns: Elements of Reusable Object-Oriented Software』 Erich Gamma著、Addison-Wesley Pub、1995年1月
