最新のgroongaに対応したRuby/groonga 0.0.2がリリースされました。
Ruby/groonga 0.0.2ではよりAPIが使いやすくなっています。
メソッドの個別化
groongaはgrn_objで抽象化されていて、ハッシュテーブルでも転置インデックスカラムでもgrn_obj_search()で検索できます。Ruby/groongaでもそれを踏襲してGroonga::Object#searchだけを定義して使いまわしていました。しかし、0.0.2ではGroonga::Hash#serachやGroonga::IndexColumn#searchなど、それぞれのオブジェクト毎に定義するようにしました。
こうすることにより以下のような挙動になるため、使いやすいAPIになりました。
- grn_obj_search()に対応していないオブジェクト(例えばGroonga::Array)に対して#searchしようとするとNoMethodErrorと適切に問題を報告する。
- 省略可能なオプション引数をより正確に検証して適切なエラーを報告する。
- それぞれの#search毎にドキュメントを用意することができるので、適切な内容のドキュメントがかかれたAPIになる。
利用できないのであれば、メソッドが定義されていない方がよいAPIだと思います。無駄なものがない方が適切なAPIに誘導しやすくなります。
無駄なものはない方がよいということは、メソッドだけではなく、省略可能なオプション引数にも言えます。1つのメソッドで何でもやろうとすると余計なオプションまで受け付ける必要があります。あるいは、余計なオプションを排除するためのコードが増えてしまいます。こうならないために、適切な粒度で別々のメソッドを定義することが有効です。
例えば、オプション名を検証するコードは以下のように書けます。(エラーメッセージに入力値と問題となった値を両方含めていることにも注意してください。問題を解決するための重要な情報です。)
def search(options={})
valid_keys = [:name, :path]
invalid_keys = options.keys - valid_keys
unless invalid_keys.empty?
message = "invalid option name(s): #{invalid_keys.inspect}: #{options.inspect}"
raise ArgumentError, message
end
end
もし、1つのメソッドでたくさんの状況を考慮しなければいけないとこのようになります。
def search(type, options={})
case type
when :fast
valid_keys = [...]
when :remote
valid_keys = [...]
else
raise ArgumentError, "invalid type: ..."
end
invalid_keys = options.keys - valid_keys
unless invalid_keys.empty?
message = "invalid option name(s): #{invalid_keys.inspect}: #{options.inspect}"
raise ArgumentError, message
end
case type
when :fast
query = options[:query]
...
when :remote
remote = DRbObject.new("druby://#{options[:host]}:2929")
remote.search(options[:query])
...
end
end
これよりは、メソッドを分けた方がすっきりします。
def fast_search(options={})
valid_keys = [...]
...
end
def remote_search(options={})
valid_keys = [...]
...
end
...
あとは、総称的なメソッドを1つ用意すればメソッド分割前と同じように使えます。
def search(type, options={})
case type
when :fast
fast_search(options)
when :remote
remote_search(options)
...
end
end
ここまできたらもう一歩です。オブジェクト指向プログラミングでcase whenやswitch caseで分岐している時はオブジェクトが足りない匂いを感じとってください。このような場合はそれぞれの条件毎にオブジェクトを作り、それぞれのオブジェクトで同じ名前のメソッドを定義します。
class FastSearcher
def search(options={})
...
end
end
class RemoteSearcher
def search(options={})
...
end
end
これで、条件分岐がなくなり、総称的なメソッドも定義しなくてもよくなります。それらは言語がやってくれるからです。
searcher = FastSearcher.new
searcher.search(:query => ...)
と、だいぶ遠回りをしましたが、Ruby/groonga 0.0.2では以上のようなAPI設計ポリシーに従って、オブジェクト毎にメソッドを実装するようになりました。これにより使いやすさが向上しています。
また、メソッドが分割されることにより、ドキュメントを書きやすくなります。読む側も読みやすくなります。
まとめ
Ruby/groonga 0.0.2はAPIの使いやすさが向上しています。これは、適切な粒度に実装を分割したからです。使いやすいAPIを検討しているのであれば、実装の粒度を細かくすることを検討してみてください。無駄がなくすっきりして使いやすいAPIになるかもしれません。