キーワード引数のかわりに fluent interface (流れるようなインターフェース) を使う

Pythonでは、キーワード引数が使える。これは、引数の種類が多い場合は非常に便利である。
理由は、可読性が高いことと、引数の順番を気にしなくていいということ。

## Pythonによるキーワード引数の例
rows = Query.find_all('stocks', where='price >= 1000',
                                order_by='name',
				limit=10)


キーワード引数がない言語では、ハッシュや連想配列を使ってそれに近いことができる。ただしキーワード引数と違って、キーワード名が間違っていてもエラーにならないという欠点がある。

## PHPによる、連想配列を使ってキーワード引数をエミュレートした例
$rows = Query.find_all('stocks', array('where'=>'price >= 1000',
                                       'order_by'=>'name',
				       'limit'=>10));


Rubyはこれを言語機能としてサポートしており、ハッシュをあたかもキーワード引数のように書くことができる。でも Ruby はもともとハッシュを簡潔に書けるから、こんなことするくらいならさっさと本物のキーワード引数をサポートして欲しいというのが正直なところ。

## Rubyによる、キーワード引数のように見えるハッシュの例
rows = Query.find_all('stocks', :where=>'price >= 1000',
                                :order_by=>'name',
				:limit=>10)


さて、これをJavaでやろうとすると、あんまりうまくいかない。理由は簡単で、Javaではjava.util.Map の記述が煩雑だから。

// Javaではjava.util.Mapの記述が簡単ではない
Map<String, Object> options = new HashMap<String, Object>();
options.put("where", "price >= 1000");
options.put("orderBy", "name");
options.put("limit", 10);
Rows rows = Query.findAll('stocks', options);


そこで、fluent interface (流れるようなインターフェース)を使う。すると、キーワード引数に負けないくらいわかりやすくオプションを指定できる。しかも、静的な型を有効に使える。

// fluent interfaceを使うと、キーワード引数と同等のわかりやすい記述ができる。
Query q = Query.findAll("stocks")
               .where("price >= 1000")
	       .orderBy("name")
	       .limit(10);
Rows rows = q.execute();


これはS2JDBCSequelや、PHPならCodeIgniterが採用している方法なので、(自分も含めて)知っている人も多いだろう。
ただ、自分の頭の中では「こういうAPIの設計方法がある」という認識しかなくて、"fluent interface" と "キーワード引数" とが結びついてなかった。

最近になってようやく「キーワード引数がたくさん出てくるようなメソッドは fluent interface を使って設計しなおすといいかもしんない」ということがわかったよ。
今回はDBクエリーの例だったけど、例えば HTML タグを生成するヘルパー関数とかもあてはまりそう。


ただ、静的な言語の場合はクラスを継承してメソッドを追加する場合に問題がある。
それは、while() や orderBy() や limit() の戻り値の型が Query のままなので、これらのメソッドの戻り値に対して追加したメソッドがそのままでは呼び出せないということだ。

// たとえば Query を継承してメソッドを追加したとしても、
// while() や orderBy() の戻り値の型は Query のままであり、
// MyQuery ではない。
class MyQuery extends Query {
  public MyQuery whereBetween(String column, int from, int to) {
    return (MyQuery)this.while(column + " between " + from + " and " + to);
  }
}

// そのため、fluent には呼び出せない (コンパイルエラーになる)。
Query q = MyQuery.findAll("stocks")       // これがMyQueryを返したとしても
                 .where("price >= 1000")  // この戻り値の型が Query なので
		 .whereBetween("price", 10000, 20000)  // これが書けない
                 .orderBy("name")
		 .limit(10);
Rows rows = q.execute();


こういう場面では、動的な言語のほうが融通が利く。