ActiveSupportを読んでみよう & テストコードに感謝する

この記事は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さんです。