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を使うといいですね。