git/GitHubを便利に使うHubを少し読む(2)
git/GitHubを便利に使うRuby製ツールのコマンドラインツール「Hub」のソースコードを少し読んだので、あれこれメモ。git/GitHubを便利に使う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
FileStoreは設定をファイルに落としたり、ロードしたりするモジュールで、Configurationは設定を扱うクラス。この2つは別ファイルに分けて良さそうな感じもあるけど、汎用のクラスではなくて、GitHubAPI用のものなので、1つのファイルに入ってるっぽい。長くないし。サブディレクトリを切って、そこに入れるという発想はあるのだろうけど、1つのファイルにどこまでクラスを含むかという粒度は好みで決まるものらしく、最近のRailsは割と細かくファイルを分ける傾向にあるような気がしてるけど、Hubみたいなツールでは「いいよ、入れちゃえよ」という傾向があるような気もしなくもない。Javaなんかだと、こういうのってインナークラスとかいうヤツでやるのかしら。
GitHubAPIの初期化のコメントで、ちょっとハッとした。
module Hub class GitHubAPI attr_reader :config, :oauth_app_url # Public: Create a new API client instance # # Options: # - config: an object that implements: # - username(host) # - api_token(host, user) # - password(host, user) # - oauth_token(host, user) def initialize config, options @config = config @oauth_app_url = options.fetch(:app_url) end
Hub::GitHubAPI.new(config, options) で初期化するわけだけど、ここで渡す config は、「以下を実装したオブジェクト」だというコメントがついている。実際、Configurationクラスは、
module Hub class GitHubAPI class Configuration def initialize store def normalize_host host def username host def api_token host, user def password host, user def oauth_token host, user, &block def prompt what def prompt_password host, user def askpass def proxy_uri(with_ssl)
となっている。何も不思議なところはないけど、「実装したクラス」とか「実装したクラスのインスタンス」ではなく「実装したオブジェクト」とコメントにあるのがちょっとおもしろいと思った。ここで渡す config って、Configuration のインスタンスではあるけど、コメントにあるとおりのメソッドに反応しさえすれば、例えば、BasicObjectを継承しただけのシングルトンでも構わないわけだ。いやいや、o = Object.new; o.extend SomeMethods とかして、本当にオブジェクトでもいいんだ。この辺が、ダックタイピングぽい発想なのかしら。
このあいだ知って、ちょっと驚いたけど、RubyのStringIOクラスとIOクラスは継承関係がない。両者のAPIはソックリで、インスタンスメソッドのインターセクションを取ってみると、
> sio = StringIO.instance_methods(false) > io = IO.instance_methods(false) > sio & io => [:reopen, :lineno, :lineno=, :binmode, :close, :close_read, :close_write, :closed?, :eof, :eof?, :fcntl, :flush, :fsync, :pos, :pos=, :rewind, :seek, :sync, :sync=, :tell, :each, :each_line, :lines, :each_byte, :bytes, :each_char, :chars, :each_codepoint, :codepoints, :getc, :ungetc, :ungetbyte, :getbyte, :gets, :readlines, :read, :write, :putc, :isatty, :tty?, :pid, :fileno, :external_encoding, :internal_encoding, :set_encoding]
という感じ。seekもできるし、posでオフセットを移動できる。ところが、StringIOはCRubyの ext/stringio/stringio.c で以下のように定義されている。
/* * Pseudo I/O on String object. */ void Init_stringio() { VALUE StringIO = rb_define_class("StringIO", rb_cData); rb_include_module(StringIO, rb_mEnumerable); rb_define_alloc_func(StringIO, strio_s_allocate); rb_define_singleton_method(StringIO, "open", strio_s_open, -1); rb_define_method(StringIO, "initialize", strio_initialize, -1); rb_define_method(StringIO, "initialize_copy", strio_copy, 1); rb_define_method(StringIO, "reopen", strio_reopen, -1);
「rb_define_class("StringIO", rb_cData);」ということで、「rb_define_class("StringIO", rb_cIO)」となっていない。StringIOはIOクラスを継承していない。
rb_cDataは、この掲示板の書き込みにあるMatz御大らしき人の説明によると、CのポインタをラップするときにRubyの拡張モジュールで使うもの、らしい。
ともあれ、StringIOは、いかにもIOを継承していて良さそうなのに継承していない。
なんでかというと、StringIOはIOから継承するべき実装が何もないから。というのがたぶん答え。Javaのようにインターフェイスを作って、それを実装するようなカチッとした言語とRubyが大きく異るのは、
class Base def initialize(foo) @foo = foo end def meth1 raise "not implemented" end def meth2 raise "not implemented" end end class Hoge < Base def meth1 something end def meth2 something else end end class Foo < Base def meth1 something end def meth2 something else end end
というようなことを決してやらない、というところ。基底クラスが空っぽの場合、そんなもん定義するだけ時間の無駄だし、無駄にコードが増えて仕様変更に弱くなるだけ、というゆるい感じがRubyの流儀らしい。反応すべきメソッドが揃ってればそれでええやん、と。だから、IOとStingIOも別に継承関係なんて要らない。一方のインスタンスを期待するコードは他方のインスタンスが渡されても、だいたい動くはず。
そんなゆるいやり方じゃ不安だと、is_a? とか、kind_of? とかして受け取った引数のクラスというか型を調べたくなるんだけど、それは静的言語のやり方を動的言語に当てはめて、動的言語の良さを台無しにする行為。動的言語の良さは何も int や char といった宣言をせずに済むというような話だけじゃなくて、こういうゆるい結合によって書かずに済むコード量にある。というようなことを、Eloquent RubyのChapter 8あたりでRuss Olsenが書いている。
動的言語はパフォーマンスを犠牲にしてまで型チェックをやらないのだから、自前でそんなチェックをやるぐらいなら静的言語を使えばいいということなんだろう。