Compositeパターンの例
コンポジット(composite)とは、他の複数のオブジェクトからなる複合物のことです。Compositeパターンは、クライアントコードが複合オブジェクトを非複合オブジェクトと同じように扱うことのできる状況で使われます。
本稿のサンプルでは、SQLの複合条件を表現するという要件をCompositeパターンでいかにスマートに実現できるかを示します。SQLの単体の条件句(例えばname like 'abc%'
など)は、and
などの接続詞を使って結合できます。従って、and
式はlike
などの句から成る複合句と考えられます。
コンポジットの組み立て方を紹介するために、まず低レベルで必要とされるオブジェクトを実装することにします。
最初にColumn
クラスで列の名前をカプセル化します。各種のSQLステートメントを構築するときに必要な列の詳細情報は、このColumn
クラスのサブクラスで定義します。例えば、StringColumn
クラスでは、SQLジェネレータオブジェクトがcreate table
ステートメントの構築に使用するかもしれないlength
フィールドを定義します。
さらに重要なことは、Column
クラスでsqlValue
という抽象メソッドを定義することです。テキスト列を表すサブクラスのメソッド実装では、SQLのwhere
句で使用する値文字列を返すようにします。例えば、where name = 'abc'
句の値文字列は'abc'
となります。一方、数値列を表すサブクラスでは、値を一重引用符で囲みません。where amount = 10
のSQL値は10
となります。
public abstract class Column { private String name; public Column(String name) { this.name = name; } String getName() { return name; } abstract String sqlValue(Object value); } public class StringColumn extends Column { private int length; public StringColumn(String name, int length) { super(name); this.length = length; } public int getLength() { return length; } public String sqlValue(Object value) { return "'" + value + "'"; } } public class NumericColumn extends Column { public NumericColumn(String name) { super(name); } public String sqlValue(Object value) { return value.toString(); } }
このような列のクラス階層ができていれば、単純な等価比較を行うwhere
句の生成テストは簡単に記述できます。
import static org.junit.Assert.*; import org.junit.*; public class EqualsTest { @Test public void stringColumn() { Column column = new StringColumn("name", 10); Equals criteria = new Equals(column, "joe"); assertEquals("name = 'joe'", criteria.sqlString()); } @Test public void numericColumn() { Column column = new NumericColumn("amount"); Equals criteria = new Equals(column, 5); assertEquals("amount = 5", criteria.sqlString()); } }
Equals
クラスの実装は次のとおりです。
public class Equals { 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)); } }
とても単純です。
2つの等価比較を組み合わせたwhere句の生成
今度は、次のような2つの等価比較を組み合わせたwhere
句を生成したいと思います。
name = 'Joe' and amount = 10
このテストを行うAndTest
クラスは次のようになります。
import static org.junit.Assert.*; import org.junit.*; public class AndTest { @Test public void and() { Equals name = new Equals(new StringColumn("name", 1), "Joe"); Equals amount = new Equals(new NumericColumn("amount"), 5); And and = new And(name, amount); assertEquals("name = 'Joe' and amount = 5", and.sql()); } }
And
クラスの実装も非常に単純です。
public class And { private Equals left; private Equals right; public And(Equals left, Equals right) { this.left = left; this.right = right; } public String sql() { return left.sqlString() + " and " + right.sqlString(); } }
同様にして、where
句で使用するその他の条件にも簡単に対応できます。例えば、like
の実装は次のようになります。
// LikeTest.java: import static org.junit.Assert.*; import org.junit.*; public class LikeTest { @Test public void simple() { Like like = new Like(new StringColumn("name", 1), "Joe%"); assertEquals("name like 'Joe%'", like.sql()); } } // Like.java: public class Like { private StringColumn column; private String value; public Like(StringColumn column, String value) { this.column = column; this.value = value; } public String sql() { return String.format("%s like %s", column.getName(), column.sqlValue(value)); } }