git/GitHubを便利に使うHubを少し読む(3)
git/GitHubを便利に使うRuby製ツールのコマンドラインツール「Hub」のソースコードを少し読んだので、あれこれメモ。これとこれの続き。
GitHubのAPIをラップする lib/hub/github_api.rb は、クラス定義関連を抜き出すと以下。
module Hub class GitHubAPI module Exceptions module HttpMethods module ResponseMethods module OAuth include HttpMethods include OAuth class FileStore class Configuration
HttpMethodsとOAuthは、定義直後に include している。ということは、これらはソースコードの読み手に意味的な構造を伝えているだけで、名前空間を切り分けているわけでもなければ、Hub::GitHubAPI以外のクラスでincludeしようとか、そういう理由から module としているわけではないのか?
試しに、以下のようにフラットな構造にして、テストを通してみる。
- module HttpMethods +# module HttpMethods # Decorator for Net::HTTPResponse module ResponseMethods def status() code.to_i end @@ -223,9 +223,9 @@ module Hub end return http end - end +# end - module OAuth +# module OAuth def apply_authentication req, url if (req.path =~ /\/authorizations$/) super @@ -253,10 +253,10 @@ module Hub res.data['token'] end end - end +# end - include HttpMethods - include OAuth +# include HttpMethods +# include OAuth
あれ、テストが落ちる。なんだろうと思ったら、module OAuthの apply_authentication では、実は super で継承ツリーの上にいるメソッドを呼んでいるのだった。関係するところを抜き出すと、
module OAuth def apply_authentication req, url if (req.path =~ /\/authorizations$/) super else user = url.user || config.username(url.host) module HttpMethods def apply_authentication req, url user = url.user || config.username(url.host) pass = config.password(url.host, user) req.basic_auth user, pass end include HttpMethods include OAuth
となっている。moduleを include するというのは、本当にメソッドをクラスに混ぜ込んでいるわけではない。includeした瞬間にRuby処理系は「Class.new」相当の無名クラスを生成して、このクラスを継承チェーンに挿入している。挿入するのは、includeするクラスと、そのクラスのスーパークラスの間。例えば、
module M def foo puts "I'm a module." end end class C include M def foo puts "I'm a class." super end end C.new.foo
としたとき、この super は、M#fooの呼び出しに相当するので、
$ ruby include.rb I'm a class. I'm a module.
となる。つまり、include順も重要ということで、
include HttpMethods include OAuth
をひっくり返すと、これまたテストがコケる。HttpMethodsとOAuthとで重複しているメソッドは、apply_authenticationだけなので、この2つを継承順に注意して逆リファクタリングすると、テストは通る。OAuthのapply_authenticationのsuperの部分に、HttpMethodsのapply_authenticationを展開して、それをHttpMethodsのほうに混ぜ込んで、moduleの階層を消す。
diff --git a/lib/hub/github_api.rb b/lib/hub/github_api.rb index 32eb3ad..0696d48 100644 --- a/lib/hub/github_api.rb +++ b/lib/hub/github_api.rb @@ -115,7 +115,7 @@ module Hub # - proxy_uri(with_ssl) # - username(host) # - password(host, user) - module HttpMethods +# module HttpMethods # Decorator for Net::HTTPResponse module ResponseMethods def status() code.to_i end @@ -197,9 +197,17 @@ module Hub end def apply_authentication req, url - user = url.user || config.username(url.host) - pass = config.password(url.host, user) - req.basic_auth user, pass + if (req.path =~ /\/authorizations$/) + user = url.user || config.username(url.host) + pass = config.password(url.host, user) + req.basic_auth user, pass + else + user = url.user || config.username(url.host) + token = config.oauth_token(url.host, user) { + obtain_oauth_token url.host, user + } + req['Authorization'] = "token #{token}" + end end def create_connection url @@ -223,21 +231,9 @@ module Hub end return http end - end - - module OAuth - def apply_authentication req, url - if (req.path =~ /\/authorizations$/) - super - else - user = url.user || config.username(url.host) - token = config.oauth_token(url.host, user) { - obtain_oauth_token url.host, user - } - req['Authorization'] = "token #{token}" - end - end +# end +# module OAuth def obtain_oauth_token host, user # first try to fetch existing authorization res = get "https://#{user}@#{host}/authorizations" @@ -253,10 +249,10 @@ module Hub res.data['token'] end end - end +# end - include HttpMethods - include OAuth +# include HttpMethods +# include OAuth # Filesystem store suitable for Configuration class FileStore
つまり、OAuthはOAuthのときのための差分を定義するモジュールということ。2つしかメソッドがない。
で、HttpMethodsが何をしているかというと、内部DSLをGitHubAPIクラスに提供している。Hub::GitHubAPIの典型的なAPIリクエストのメソッドは、以下のような感じになる。
# Public: Fork the specified repo. def fork_repo project res = post "https://%s/repos/%s/%s/forks" % [api_host(project.host), project.owner, project.name] res.error! unless res.success? end
GitHubへのAPIコールは、すべてのこの形式で抽象化されている。文字列に変数を評価して埋め込むとき、#{} を使ったインターポレーションをすることがRubyでは多いけど、複数のデータを渡すなら、このように、String#% で流しこむのも見やすくていいかもなと思った。
ポイントは、
- HTTP POST/GET は、post/getというメソッド名で抽象化されている
- projectというオブジェクトは実際には色々あって、ダックタイピングの典型ぽい
というところかなと思う。
で、例えば、getのほうは、
def get url, &block perform_request url, :Get, &block end
となっていて、さらにperform_requestを呼んでいる。パラメータで渡されている :Get は、
def perform_request url, type url = URI.parse url unless url.respond_to? :host require 'net/https' req = Net::HTTP.const_get(type).new request_uri(url)
のように、Net::HTTP::Getのクラスへの参照を持つ定数を引っ張りだすときに、const_getのリフレクションを利用する場面で使われている。
github_api.rb を見ていて改めて思ったけど、どんどん細かくメソッドを分けることで挙動に名前をつけるというのが、Rubyっぽい。例えば、コマンドラインから与えられたプロジェクトが存在するかどうかを確かめる GitHubAPI#repo_exists? というメソッドは以下のように、1行になっている。
# Public: Fetch data for a specific repo. def repo_info project get "https://%s/repos/%s/%s" % [api_host(project.host), project.owner, project.name] end # Public: Determine whether a specific repo exists. def repo_exists? project repo_info(project).success? end
repo_infoを呼び出して、それが success? かどうかを確かめている。この success? というのは、net/httpのNet::HTTPResponseに混ぜ込むために用意してある、Hub::GitHubAPI::HTTPMethods::ResponseMethodsに以下のように定義されている。
module HttpMethods # Decorator for Net::HTTPResponse module ResponseMethods def status() code.to_i end def data?() content_type =~ /\bjson\b/ end def data() @data ||= JSON.parse(body) end def error_message?() data? and data['errors'] || data['message'] end def error_message() error_sentences || data['message'] end def success?() Net::HTTPSuccess === self end
実際にこのモジュールは、perform_requestで、
res = http.start { http.request(req) }
res.extend ResponseMethods
という風に利用されている。HTTPリクエストを出して、戻ってきたレスポンスに対して、、、毎回そのオブジェクトにデコレータとしてのメソッドを混ぜ込んでいる。
いくらなんでも毎回混ぜ込むとかCPUリソースを何だと思ってるんや! という気も一瞬したけど、Hubは人間の使うコマンドラインツールなので、ハッキリ言ってIOの絡まないCPUリソースだけの処理時間なんて誤差もいいところ。インターネットで行ってこいするHTTPリクエストのラウンドトリップが所要時間を決定するので、こういう動的なやり方のパフォーマンス上のデメリットを気にするやつは豚に食われて死ねってことか。
ところで、1行の定義で済むメソッドを1行に書くときに、
class Foo def foo; puts "foo" end end class Foo def foo() puts "foo" end end
の2つの流儀があり得る。ぼくは前者を使っていた。それは「続けて書いたらRubyのパーザには分かるまい」という配慮だったけど、考えてみれば、元々メソッド呼び出しのときのカッコ・コッカを省略しているという話なのだし、読み手であるプログラマにとっての分かりやすさを考えると、カッコ・コッカを入れたほうがいいような気もしてきた。それによって、Ruby処理系にも分かるし、人間にも分かる。
カッコ・コッカのほうがセミコロンよりも、Ruby特有の事情がないぶんプログラマ一般での通用度が高いような気もするけど、でもセミコロンが文末区切りの明示に使われるのだというのも別に不自然でも分かりづらいわけでもない。この辺は単に取り決めの問題かしら。