Railsのプラグインを作る基礎テク

developmentモードとproductionモード

今日チームのメンバーに聞かれて気付いたことですが、developmentモードが便利過ぎるからか、リクエストが来るたびにコントローラのクラスがロードされるように勘違いする人もいるみたいです。

プラグインを作るときにはproductionモードとdevelopmentモードの違いを把握しておかないと、プラグインを作るのに困るので、念のため書いておきます。

考え方としては、基本はproductionモードです。productionモードで各ファイルは基本的に一度しかロードされません。それだとコードを変更するたびに反映させるためにはにいちいちサーバーを再起動させなければならないので、開発時はあまりにも大変なので、Railsのdeveloppmentモードでは特殊な仕組みで、リクエストが来るたびに必要なファイルをロードします。

const_missingと命名規則

Rubyではファイルをロードするためには、通常requireあるいはloadメソッドを使いますが、通常Railsアプリではモデルやコントローラの定義してあるファイルをrequireする必要はありません。

これは、どうなっているかというとRubyにはModule#const_missing( http://www.ruby-lang.org/ja/man/html/Module.html#const_missing )というメソッドありまして、参照しようとした定数が存在しないときに呼び出されるようになっています。

Railsではこのconst_missingメソッドを拡張して、参照しようとした定数がないと、命名規則からそれが定義されているはずのファイル名を求めそれをロード(developmentモードではload、productionモードではrequire)します。

この辺の実装は、activesupportのlib/active_support/dependencies.rbにあります。

alias_method_chain

通常のプラグインでは、モジュール(クラスを含む)で宣言されているメソッドの振る舞いを変更する場合、alias_method_chainを使います。これはactivesupportのlib/active_support/core_ext/module/aliasing.rbに定義されているメソッドで、以下のようなことができます。

require 'rubygems'
require 'active_support'
class A
  def foo
    "foo"
  end

  def foo_with_hoge
    foo_without_hoge + " with HOGE"
  end

  alias_method_chain :foo, :hoge
end
a = A.new
a.foo
# => "foo with HOGE"
a.foo_with_hoge
# => "foo with HOGE"
a.foo_without_hoge
# => "foo"

実はalias_method_chainを使わなくてもこんな書き方でも同じようなことができます。

  # alias_method_chain :foo, :hoge
  alias_method :foo_without_hoge, :foo
  alias_method :foo, :foo_with_hoge

alias_methodは元々Rubyにあるメソッドです。http://www.ruby-lang.org/ja/man/html/Module.html#alias_method

要は元々あるメソッドに_without_hogeを後ろにくっつけた名前で別名を付けて、拡張するメソッドを元々のメソッドの名前の別名を付けて上書きしちゃいます。

モジュールの基礎

Railsプラグインを作る上で、Rubyの基本的なところがやっぱり重要なので、その辺もまとめておきます。

Module#included

module Hoge
  def foo_with_hoge
    foo_without_hoge + "with HOGE"
  end
end

というモジュールがあって、

class Foo
  include Hoge
  def foo
    "foo"
  end
  alias_method_chain :foo, :hoge  
end

という風に拡張させることができますが、alias_method_chainの行を書くために、これではFooを実装する人がHogeの中身をある程度知っている必要が生じてしまいます。そういうことはできるだけ少ない方が楽ちんなので、こんな風に書き換えます。

module Hoge
  def self.included(klass)
    klass.module_eval do
      alias_method_chain :foo, :hoge
    end
  end

  def foo_with_hoge
    foo_without_hoge + "with HOGE"
  end
end

という風にincludedという特異メソッドを定義してあげると

class Foo
  include Hoge
  def foo
    "foo"
  end
end

Fooの方はinclude Hogeと書くだけでfooメソッドを拡張することができます。

module_evalとinstance_eval

module_evalはModuleのメソッド、instance_evalはObjectのメソッドです。

instance_eval

対象のオブジェクトに対して直接操作するものです。

Time.instance_eval do
  def now_utc
    self.now.utc
  end
end
Time.now_utc
# => Thu Feb 18 16:17:32 UTC 2010

このnow_utcメソッド内のselfはTimeクラス自身を指します。

module_eval

モジュール(クラスも含む)に対して、そのインスタンス向けのメソッドなどを定義したりする操作を行います。

Time.module_eval do
  def utc_strftime(*args)
    self.utc.strftime(*args)
  end
end
Time.new.strftime('%c')
# => "Fri Feb 19 01:25:58 2010"
Time.new.utc_strftime('%c')
# => "Thu Feb 18 16:25:48 2010"

要は、普通にclassやmoduleで定義する時と同じで、このutc_strftime内のselfはTime.newで生成されたTimeのインスタンスです。

クラスはモジュール

http://www.ruby-lang.org/ja/man/html/Class.html にもある通り、ClassのスーパークラスはModuleです。

だからTimeでmodule_evalが使えるのです。Time自身はClassのインスタンスなので、Classの継承元であるModuleで用意されているmodule_evalが使えます。

モジュールに対してmodule_evalは不自然だからか、実はclass_evalというメソッドもありますが、これはModuleのメソッドで、module_evalの別名です。

クラスもObject

ClassはModuleを継承したものですが、Moduleの継承元はObjectです。なので、もちろんクラスやモジュールでもObjectのメソッドが使用できます。
なので、Timeに対してObjectのメソッドであるinstance_evalが使える訳です。

特異メソッド

Javaなどの静的な型付け言語から入るとメソッドの定義はクラス(あるいはそれに似たもの)に対して行うものと思ってしまいがちですが、Rubyではメソッドはクラスやモジュールを使わなくても定義できます。

obj = Object.new
def obj.foo
  "foo"
end
obj.foo
# => "foo"

このobj専用のメソッドであるfooを普通に定義したメソッドと区別して「特異メソッド」と呼びます。
これをinstance_evalを使って書くとこんな感じ。

obj = Object.new
obj.instance_eval do
  def foo
    "foo"
  end
end
obj.foo
# => "foo"

例えば、このobjをTimeに置き換えてみます。

Time.instance_eval do
  def foo
    "foo"
  end
end
Time.foo
# => "foo"

instance_evalを使わずに書くと

class Time
  def self.foo
    "foo"
  end
end
Time.foo
# => "foo"

これは見たことありますよね?Timeのクラスメソッドを定義しています。
つまりクラスメソッドはTimeというオブジェクトの特異メソッドなわけです。

ちなみにこの特異メソッドを定義するための特殊な書き方がこれ。

class Time
  class << self
    def foo
      "foo"
    end
  end
end
Time.foo
# => "foo"

"class <<"という書き方ですが、別にクラスにしか使えない訳ではありません。

obj = Object.new
class << obj
  def foo
    "foo"
  end
end
obj.foo
# => "foo"

特異メソッドのエイリアス

上の方の話で、インスタンスメソッドに対してalias_methodを使って別名を付けることをしていましたが、特異メソッドのエイリアスについてはModule#alias_methodではできません。

obj = Object.new
class << obj
  def foo
    "foo"
  end
end
obj.foo
# => "foo"

という定義があったときに、fooを拡張したい場合

obj.instance_eval do
  def foo_with_hoge
    foo_without_hoge + " with HOGE"
  end
  
  alias :foo_without_hoge :foo
  alias :foo :foo_with_hoge
end
obj.foo
# => "foo with HOGE"

という風に、aliasを使うことで実現できます。
alias_methodはModuleのインスタンスメソッドでしたが、aliasはメソッドではありません。メソッドに関する別名を付ける記述方法なので、メソッドと違いカンマで区切らないので要注意です。
http://www.ruby-lang.org/ja/man/html/_A5AFA5E9A5B9A1BFA5E1A5BDA5C3A5C9A4CEC4EAB5C1.html#alias

Object#extend

http://www.ruby-lang.org/ja/man/html/Object.html#extend
特異メソッドをまとめてモジュールにして、それをextendすることで特異メソッドを定義することができます。

obj = Object.new
class << obj
  def foo
    "foo"
  end
end
obj.foo
# => "foo"

があった場合に

module SingletonMethods
  def self.extended(obj)
    obj.instance_eval do
      alias :foo_without_hoge :foo
      alias :foo :foo_with_hoge
    end
  end
  
  def foo_with_hoge
    foo_without_hoge + " with HOGE"
  end
end
obj.extend(SingletonMethods)
obj.foo
# => "foo with HOGE"

という風にextendedを定義すると、前述のインスタンスのように特異メソッドも拡張することができます。

よくあるパターン

まとめになりますが、例えばこんなクラスがあった場合

class A
  def self.foo
    "foo"
  end
  
  def bar
    "bar"
  end
end
A.foo
# => "foo"
A.new.bar
# => "bar"

これを拡張するモジュールはこんな感じで書くことがよくあります。

module Hoge
  def self.included(mod)
    mod.extend(ClassMethods)
    # ClassMethods.extendedを用意してそこに書いても良いんですけど、面倒なのでここにまとめちゃいます。
    mod.instance_eval do
      alias :foo_without_hoge :foo
      alias :foo :foo_with_hoge
    end
    mod.module_eval do
      alias_method :bar_without_hoge, :bar
      alias_method :bar, :bar_with_hoge
    end
  end

  module ClassMethods
    def foo_with_hoge
      foo_without_hoge + " with HOGE"
    end
  end

  def bar_with_hoge
    bar_without_hoge + " with HOGE"
  end
end

という拡張を用意して、

A.module_eval do
  include Hoge
end

で、クラスに拡張を適用すると

A.foo
# => "foo with HOGE"
A.new.bar
# => "bar with HOGE"

という風に動いちゃうわけです。

ここのalias_methodを使っている部分は、Railsなどのactive_supportを使っている環境だったらalias_method_chainを使うといいですね。

まとめ

alias_methodとaliasをうまく使うと既存のクラスのメソッドを拡張できて、それらの拡張もモジュールを使うと分かりやすくまとめられます。
この辺を頭に入れた上でいろんなプラグインRails本体のコードを読むとより理解が深まると思います。
で、ガリガリプラグインを書きましょう。

変なところがあったらご指摘くださいませ。