この記事はRuby Advent Calendar 2011の15日目になります。
14日目の記事は、ongaeshi007さんによる、RubyGemsはrequireの裏で何をやっているのか? - ブログのおんがえしです。
タイトルと関係無い話
全くの私事で恐縮ですが、本日、2年半とちょっと勤めていた会社を退職しました。
いわゆる大手SIerという所で、Excelと睨めっこし、誰が読むんだから良く分からない資料を書いたりしていましたが
来週から、Ruby/Railsで仕事できるみたいです。なんたる僥倖。
しばらくは、立場上フリーということになるので、
調べておかないといけないことや、やらなきゃいけないことが一杯あるんですが、
段階的に開かれる送別会と、忘年会で飲み会だらけで、一向に手が進んでなかったりします。
まだ、そんな先のことは分かりませんが、
胸張ってエンジニアですよと言えるような自分になれるように
これからも色々学んでいかなければ、と思います。
こういう機会が得られたことは、コミュニティや勉強会で知り合った皆さんのおかげです。
この場を借りて、お礼申し上げます。
ありがとうございます。本年はお世話になりました。
そして、来年もよろしくお願いします!
ActiveSupportとは
さて、本編に入りたいと思います。
ほとんどの人はご存知だと思いますが、ActiveSupportとはどういうものか、というと、
一言で言えば、Railsでコードを書く時に、組み込みのコアクラス群に
便利な拡張機能を付け足してくれるライブラリです。
有名なのだと、"post".pluralizeとか。
ただ、Railsの上でぼんやり使ってるだけだと、見逃している便利な機能があるのではということで、
以前に有志によるActiveSupportソースコードリーディング@渋谷という感じで、
ActiveSupportの中身を見ながら、細かい部分を再確認してみようという勉強会に参加してきました。
今回は、ActiveSupportの中でコア拡張とはちょっと外れた部分を
ソースコードを見ながら一部紹介してみたいと思います。
ActiveSupportを読む
ActiveSupportの入口
ActiveSupportを普通にrequireした時の入口はこんな感じになってます。
# lib/active_support.rb #module部分のみ抜粋 module ActiveSupport class << self attr_accessor :load_all_hooks def on_load_all(&hook) load_all_hooks << hook end def load_all!; load_all_hooks.each { |hook| hook.call } end end self.load_all_hooks = [] on_load_all do [Dependencies, Deprecation, Gzip, MessageVerifier, Multibyte] end end require "active_support/dependencies/autoload" require "active_support/version" module ActiveSupport extend ActiveSupport::Autoload autoload :DescendantsTracker autoload :FileUpdateChecker autoload :LogSubscriber autoload :Notifications # TODO: Narrow this list down eager_autoload do autoload :BacktraceCleaner autoload :Base64 autoload :BasicObject autoload :Benchmarkable autoload :BufferedLogger autoload :Cache autoload :Callbacks autoload :Concern autoload :Configurable autoload :Deprecation autoload :Gzip autoload :Inflector autoload :JSON autoload :Memoizable autoload :MessageEncryptor autoload :MessageVerifier autoload :Multibyte autoload :OptionMerger autoload :OrderedHash autoload :OrderedOptions autoload :Rescuable autoload :StringInquirer autoload :TaggedLogging autoload :XmlMini end autoload :SafeBuffer, "active_support/core_ext/string/output_safety" autoload :TestCase end
この中から面白そうなモジュールを適当に選んでみます。
ActiveSupport::Autoload
まず最初にActiveSupportのモジュール空間で、extendされている::Autoloadを見てみます。
# lib/active_support/dependencies/autoload.rb require "active_support/inflector/methods" require "active_support/lazy_load_hooks" module ActiveSupport module Autoload @@autoloads = {} @@under_path = nil @@at_path = nil @@eager_autoload = false def autoload(const_name, path = @@at_path) full = [self.name, @@under_path, const_name.to_s, path].compact.join("::") location = path || Inflector.underscore(full) if @@eager_autoload @@autoloads[const_name] = location end super const_name, location end #... To Be Continued
組込みのautoloadを拡張し、ライブラリのファイルパスを省略しても、
定数名からファイルパスを勝手に構築してくれるようにしています。
さっきのActiveSupportの入口のautoloadは全てこうやって呼び出されています。
自分で階層構造になっているライブラリを書く時、記述量を減らすことができそうです。
ついでに、lazy_load_hooksを見てみたいと思います。
コード本体は省略して、コメントに書かれている使い方だけ載せます。
# lib/active_support/lazy_load_hooks.rb # lazy_load_hooks allows rails to lazily load a lot of components and thus making the app boot faster. Because of # this feature now there is no need to require <tt>ActiveRecord::Base</tt> at boot time purely to apply configuration. Instead # a hook is registered that applies configuration once <tt>ActiveRecord::Base</tt> is loaded. Here <tt>ActiveRecord::Base</tt> is used # as example but this feature can be applied elsewhere too. # # Here is an example where +on_load+ method is called to register a hook. # # initializer "active_record.initialize_timezone" do # ActiveSupport.on_load(:active_record) do # self.time_zone_aware_attributes = true # self.default_timezone = :utc # end # end # # When the entirety of +activerecord/lib/active_record/base.rb+ has been evaluated then +run_load_hooks+ is invoked. # The very last line of +activerecord/lib/active_record/base.rb+ is: # # ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base)
ActiveSupport.onloadで、名前を付けて、ブロックを渡していくと、
それが中で@load_hooksというテーブルにプッシュされていきます。
後で、ActiveSupport.run_load_hooksで、登録名と、実行したいスコープを指定して実行すると、
名前に関連付けられたブロックが全て指定したオブジェクトのスコープで実行されます。
コメント分にも書いてある通り、これを利用することでActiveRecordのような巨大なライブラリを
読み込むタイミングを考慮することなく、先に関連する設定を列挙しておいて、
読み込みが完了した時点で貯めていた設定を適用して初期化したりオブジェクトを拡張したりできます。
https://github.com/amatsuda/kaminari/blob/master/lib/kaminari/hooks.rbなどで、
この機能が活用されているのが分かります。
ActiveSupport::Callbacks
それでは、上に戻って次の気になるモジュールを見てみます。
名前で大体想像が付くと思いますが、
これをincludeすることで、コールバックを利用するメソッドを簡単に定義できるようになります。
コードを載せるには長すぎるので、これもコメントだけ抜粋します。
# lib/active_support/callbacks.rb # ==== Example # # class Record # include ActiveSupport::Callbacks # define_callbacks :save # # def save # run_callbacks :save do # puts "- save" # end # end # end # # class PersonRecord < Record # set_callback :save, :before, :saving_message # def saving_message # puts "saving..." # end # # set_callback :save, :after do |object| # puts "saved" # end # end # # person = PersonRecord.new # person.save # # Output: # saving... # - save # saved
基本的な使い方は見れば分かりそうですね。
数行のコードでコールバック機構を自前のクラスに組込むことができます。
定義時にいくつかオプションを渡したり、条件指定をして動作を制御することも色々可能です。
少し長くなりますが、テストコードに定義の例が色々と記述されているので抜粋してみます。
# test/callbacks_test.rb class Record include ActiveSupport::Callbacks define_callbacks :save def self.before_save(*filters, &blk) set_callback(:save, :before, *filters, &blk) end def self.after_save(*filters, &blk) set_callback(:save, :after, *filters, &blk) end class << self def callback_symbol(callback_method) method_name = :"#{callback_method}_method" define_method(method_name) do history << [callback_method, :symbol] end method_name end def callback_string(callback_method) "history << [#{callback_method.to_sym.inspect}, :string]" end def callback_proc(callback_method) Proc.new { |model| model.history << [callback_method, :proc] } end def callback_object(callback_method) klass = Class.new klass.send(:define_method, callback_method) do |model| model.history << [:"#{callback_method}_save", :object] end klass.new end end def history @history ||= [] end end class Person < Record [:before_save, :after_save].each do |callback_method| callback_method_sym = callback_method.to_sym send(callback_method, callback_symbol(callback_method_sym)) send(callback_method, callback_string(callback_method_sym)) send(callback_method, callback_proc(callback_method_sym)) send(callback_method, callback_object(callback_method_sym.to_s.gsub(/_save/, ''))) send(callback_method) { |model| model.history << [callback_method_sym, :block] } end def save run_callbacks :save end end class PersonSkipper < Person skip_callback :save, :before, :before_save_method, :if => :yes skip_callback :save, :after, :before_save_method, :unless => :yes skip_callback :save, :after, :before_save_method, :if => :no skip_callback :save, :before, :before_save_method, :unless => :no def yes; true; end def no; false; end end # .. To Be Continued
ActiveRecordのようなbefore_saveや、after_saveといったメソッドの定義の仕方や、
特定の条件で、コールバックをスキップする場合等の定義の仕方が記述されています。
内部動作を細かく追いかけるには、ちょっとハードなので、
今回の記事ではこれ以上進みませんが、利用方法としてはこれで十分活用できそうですね。
自前のクラスにバリデーション仕込んだり、動作ログをしかけたりと活用できそうです。
ActiveSupport::OptionMerger
これは、結構渋いモジュールです。
こんな感じで定義されています。
# lib/activesupport/option_merger.rb require 'active_support/core_ext/hash/deep_merge' module ActiveSupport class OptionMerger #:nodoc: instance_methods.each do |method| undef_method(method) if method !~ /^(__|instance_eval|class|object_id)/ end def initialize(context, options) @context, @options = context, options end private def method_missing(method, *arguments, &block) if arguments.last.is_a?(Proc) proc = arguments.pop arguments << lambda { |*args| @options.deep_merge(proc.call(*args)) } else arguments << (arguments.last.respond_to?(:to_hash) ? @options.deep_merge(arguments.pop) : @options.dup) end @context.__send__(method, *arguments, &block) end end end
見た感じ、実行コンテキストとオプションを指定してインスタンス化し、
OptionMergerから呼ばれたメソッドは、オプションを追加した上で、
実行コンテキストに渡すという動作をしているようです。
いまいち、使い方がピンと来なかったので、これもテストコードを見てみます。
# test/option_merget_test.rb require 'abstract_unit' require 'active_support/core_ext/object/with_options' class OptionMergerTest < Test::Unit::TestCase def setup @options = {:hello => 'world'} end def test_method_with_options_merges_options_when_options_are_present local_options = {:cool => true} with_options(@options) do |o| assert_equal local_options, method_with_options(local_options) assert_equal @options.merge(local_options), o.method_with_options(local_options) end end def test_method_with_options_appends_options_when_options_are_missing with_options(@options) do |o| assert_equal Hash.new, method_with_options assert_equal @options, o.method_with_options end end # .. To Be Continued private def method_with_options(options = {}) options end
どうやらOptionMergerは、コア拡張のwith_optionsで利用されているクラスのようです。
これだけでもある程度分かりますが、with_optionsの定義も見てみると、
以下のような利用方法が書いてあります。
class Account < ActiveRecord::Base with_options :dependent => :destroy do |assoc| assoc.has_many :customers assoc.has_many :products assoc.has_many :invoices assoc.has_many :expenses end end
同じオプションを渡し続けるような時にブロックでまとめてしまおうって感じですね。
さりげない可読性の向上を担っている感じで、なんかいぶし銀な雰囲気を感じます。
ActiveSupport::OrderedOptions
オプション系でもう一つ。
中身も利用法も簡単なので、さくっと引用だけ。
# こんなのが h = {} h[:boy] = 'John' h[:girl] = 'Mary' h[:boy] # => 'John' h[:girl] # => 'Mary' # こう書ける h = ActiveSupport::OrderedOptions.new h.boy = 'John' h.girl = 'Mary' h.boy # => 'John' h.girl # => 'Mary'
ソースコードを読む時のテストコードの有用性
今回、この記事を書くにあたってざっくりと、ActiveSupportの中身を読んでみました。
実際は、もっとクールな機能が一杯あるのですが、まとめ的に説明するには、
ボリュームがありすぎるので、ある程度簡単に解説できるものを選んでみました。
今回読んでみて、そしてソースコードリーディング会の時にも実感しましたが、
ソースコードを読む時には、テストコードを活用すると理解が一気に容易になります。
抽象度の高いモジュールや、細かく動作を制御できるようなモジュール等は、
コメントとソースコードだけで、動作を把握するのが難しいことがあります。
(私のリーディングパワーが足りないだけかもしれませんが)
そこでテストコードです。
この記事でも、そういう流れが見えるような書き方をしてみました。
テストコードには、そのモジュールの呼び出し方と、期待する結果がセットで書かれているので、
これをこうするとこうなるというのが、一つのファイルで参照できます。
テストコードはただコードがちゃんと動くことを検証するためのものではなく、
プログラマーにとってドキュメントと同様の性質を持っています。
これは言い換えると、テストコードをちゃんと書いておけば、
他の人や、未来の自分がコード見る時に、コードを理解しやすくなるということです。
プロダクトの質のためだけでなく、将来の自分や他の誰かの理解のためにも、
テストコードを書くのは役に立つことです。
テスト大事。これはRubyの文化です。
次のRuby Advent Calendar 2011
Ruby Advent Calendar 2011
次回は、16日目 yoppiさんです。