RailsのMapperあたりを読んでみる
「Head First Rails」に続いて、「RailsによるアジャイルWebアプリケーション開発 第3版」をツラツラと読んでみた。ホビープログラマとしては、テストの話あたりが新鮮で面白かった。
まずURLのラウティング(ディスパッチ?)処理をやってるActionController::Routing::Routesがよく分からない。connectとか、resource、resourcesって何なのよ、結局! ということで、いっそソースコードを見てみようと思ってrouting.rbとかroute_set.rbを覗いてみたら、これが結構面白く、かつ、ああ、Rubyのメタプログラミング方面がよくワカランなぁということが分かった。
rails/app/config/route.rbの記法に関連したキモの1つは、
def method_missing(route_name, *args, &proc) #:nodoc: super unless args.length >= 1 && proc.nil? @set.add_named_route(route_name, *args) end
というところか。Mapperオブジェクトで定義されてるデフォルトのrouteは、rootだけなので、
map.login 'login', :controller => 'accounts', :action => 'login'
とかやると、
Mapper.method_missing(:login, {:controller => 'accounts', :action => 'login'})
て感じに呼ばれてるってことか。
ほかのCatalystとかDjangoでは、このへんどうなってるのかと思ってググッてみたら、少なくともAPI的には似たり寄ったりな感じのようで、なるほどなと思った。
やっぱりコードを読むといろいろと勉強になるもので、例えばActionController::Routing#normalize_pathsは、
def normalize_paths(paths) # do the hokey-pokey of path normalization... paths = paths.collect do |path| path = path. gsub("//", "/"). # replace double / chars with a single gsub("\\\\", "\\"). # replace double \ chars with a single gsub(%r{(.)[\\/]$}, '\1') # drop final / or \ if path ends with it # eliminate .. paths where possible re = %r{[^/\\]+[/\\]\.\.[/\\]} path.gsub!(re, "") while path.match(re) path end # start with longest path, first paths = paths.uniq.sort_by { |path| - path.length } end
なんてある。コレクションを破壊的に書き換えるとき、どういうスタイルがいいんだろうかと思ったりしていたけど、map系でブロックで渡して戻り値をそのまま代入するというスタイルでいいのか、というのが発見。何となくブロックを受け取るイテレータっぽいやつって戻り値とかじゃなくて、言語キーワード的に感じていたから、キーワードに戻り値なんてないような印象を持っていた、ような気がする。
スタイルにしても、改行の位置やgsubという同類のメソッドチェーンの並べ方なんかがスッキリ読みやすい。
似たような話で、
def generation_extraction segments.collect do |segment| segment.extraction_code end.compact * "\n" end
というのも新鮮。endの後ろにメソッドをくっつけるのとか、良くないのかと思ってたけど、ぜんぜんふつうなのか。
もう1つナルホドと思ったのは、Routeオブジェクトを初期化が終わったら、freezeしてしまうというところだけど、
def to_s @to_s ||= begin segs = segments.inject("") { |str,s| str << s.to_s } "%-6s %-40s %s" % [(conditions[:method] || :any).to_s.upcase, segs, requirements.inspect] end end # TODO: Route should be prepared and frozen on initialize def freeze unless frozen? write_generation! write_recognition! prepare_matching! parameter_shell significant_keys defaults to_s end super end
初期化のときに世代管理やらto_sをあらかじめ用意しておくとかをやっている。ここでも、to_sの値をメモしておく「@to_s ||= begin」とブロックの戻り値を代入してる。
それより以下のようなコードがよく分からないけど、なんだか面白そうだ。Moduleのメソッドを全部undefして、module_evalでゴソゴソとなにやら足している。こういうのがメタプログラミングなのかなと想像しつつ、やっぱりそのへん全然分かってないなぁという感じが強い。
class NamedRouteCollection #:nodoc: include Enumerable include ActionController::Routing::Optimisation attr_reader :routes, :helpers def initialize clear! end def clear! @routes = {} @helpers = [] @module ||= Module.new @module.instance_methods.each do |selector| @module.class_eval { remove_method selector } end end def add(name, route) routes[name.to_sym] = route define_named_route_methods(name, route) end def get(name) routes[name.to_sym] end alias []= add alias [] get alias clear clear! def each routes.each { |name, route| yield name, route } self end def names routes.keys end def length routes.length end def reset! old_routes = routes.dup clear! old_routes.each do |name, route| add(name, route) end end def install(destinations = [ActionController::Base, ActionView::Base], regenerate = false) reset! if regenerate Array(destinations).each do |dest| dest.__send__(:include, @module) end end private def url_helper_name(name, kind = :url) :"#{name}_#{kind}" end def hash_access_name(name, kind = :url) :"hash_for_#{name}_#{kind}" end def define_named_route_methods(name, route) {:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts| hash = route.defaults.merge(:use_route => name).merge(opts) define_hash_access route, name, kind, hash define_url_helper route, name, kind, hash end end def define_hash_access(route, name, kind, options) selector = hash_access_name(name, kind) @module.module_eval <<-end_eval # We use module_eval to avoid leaks def #{selector}(options = nil) options ? #{options.inspect}.merge(options) : #{options.inspect} end protected :#{selector} end_eval helpers << selector end
そういえば、RailsのActiveSupport関連の約4000個あるファイルのうち2000個ほどにも目を通してみた。lessで順々に読んでいっただけだけど、何となく面倒なのでTextMateを買ってみた。Emacsと似たようなもんだろうと思ってたけど、結構いろいろ違う。