includeする時にクラスメソッドの呼び出しと定義を行うmodule書式
今までrestful_authenticationは以下のURLを指定してインストールしていたのだが...
script/plugin install http://svn.techno-weenie.net/projects/plugins/restful_authentication/
ふと思い立って、プラグイン名だけ指定してインストールコマンドを実行してみた。(実は以前も試していて、その時はいつまで経っても反応がなく、諦めていたのだった。)
script/plugin install restful_authentication
-
-
- 今試すとhttp://svn.techno-weenie.net/projects/plugins/restful_authentication/と同じバージョンがインストールされた...。進化洗練バージョンは以下から。
- http://github.com/technoweenie/restful-authentication/tree/master (ruby script/plugin install git://github.com/technoweenie/restful-authentication.gitがスマート -- 夜のDiscoveryさんより)
- restful_authenticationには現在5つのブランチがあるようだ。(classic、master、merbful_authentication、modular、rails-1.2)
- 今まで使っていたのはclassicで、進化洗練されていると思ったのはmasterだった。
-
すると、問題もなく正常にインストールされてしまった。
中身を確認してみると、何と!今までよりも進化洗練されているようだ。
- パスワードを保存する時のセキュリティが、より安全になったようだ。
- モジュールが階層化され、目的毎に分類されている。
ソースコードを読んでみて、早速立ち止まってしまったのがモジュールの書き方。例えばAuthentication::ByPasswordは以下のように定義されている。
module Authentication module ByPassword # Stuff directives into including module def self.included( recipient ) recipient.extend( ModelClassMethods ) recipient.class_eval do include ModelInstanceMethods # Virtual attribute for the unencrypted password attr_accessor :password validates_presence_of :password, :if => :password_required? validates_presence_of :password_confirmation, :if => :password_required? validates_confirmation_of :password, :if => :password_required? validates_length_of :password, :within => 6..40, :if => :password_required? before_save :encrypt_password end end # #included directives # # Class Methods # module ModelClassMethods # This provides a modest increased defense against a dictionary attack if # your db were ever compromised, but will invalidate existing passwords. # See the README and the file config/initializers/site_keys.rb # # It may not be obvious, but if you set REST_AUTH_SITE_KEY to nil and # REST_AUTH_DIGEST_STRETCHES to 1 you'll have backwards compatibility with # older versions of restful-authentication. def password_digest(password, salt) digest = REST_AUTH_SITE_KEY REST_AUTH_DIGEST_STRETCHES.times do digest = secure_digest(digest, salt, password, REST_AUTH_SITE_KEY) end digest end end # class methods # # Instance Methods # module ModelInstanceMethods # Encrypts the password with the user salt def encrypt(password) self.class.password_digest(password, salt) end def authenticated?(password) crypted_password == encrypt(password) end # before filter def encrypt_password return if password.blank? self.salt = self.class.make_token if new_record? self.crypted_password = encrypt(password) end def password_required? crypted_password.blank? || !password.blank? end end # instance methods end end
Authentication::ByPasswordの書式の骨格
モジュールの中にモジュールを定義して、それをextendしたり、class_evalブロックの中でincludeしたり、いったい何をやっているのかと?書式の骨格部分だけ抜き出してみると以下のようになっていた。
module Authentication module ByPassword def self.included( recipient ) recipient.extend( ModelClassMethods ) recipient.class_eval do include ModelInstanceMethods end end module ModelClassMethods end # class methods module ModelInstanceMethods end # instance methods end end
include Authentication::ByPasswordで何が起こっているのか?
Authentication::ByPasswordはUserモデルの中でincludeされている。
...(中略)... class User < ActiveRecord::Base include Authentication include Authentication::ByPassword include Authentication::ByCookieToken ...(中略)...
- includeが実行されると...
- Authentication::ByPasswordモジュールでは、引数recipientにUserが代入される。
- User.extend( ModelClassMethods )によって、module ModelClassMethodsに定義されたことはクラスメソッドになる。
- User.class_eval doブロック内のinclude ModelInstanceMethodsによって、ModelInstanceMethodsに定義されたことはインスタンスメソッドになる。
module Authentication::ByPassword def self.included( recipient ) User.extend( ModelClassMethods ) User.class_eval do include ModelInstanceMethods # クラスメソッド呼び出し end end module ModelClassMethods # クラスメソッド定義 end # class methods module ModelInstanceMethods # インスタンスメソッド定義 end # instance methods ...(中略)...
- User.class_eval doブロック内のコードは、Userクラスに直接コードを書くことと同等なので、
User.class_eval do validates_presence_of :password end
- 上記コードは、以下と同等の結果をもたらすと思う。(クラスメソッドの呼び出し)
class User validates_presence_of :password end
つまり、普通はインスタンスメソッドの拡張しか行わないincludeで、以下3つの処理を行うための、便利な定型書式だったのである。
- クラスメソッドの呼び出し
- クラスメソッド定義
- インスタンスメソッド定義
self.includedとは何か?
今まで曖昧なままだったので調べてみると、includeした時にappend_featuresメソッドと共にincludedメソッドも呼び出される仕組みようだ。その部分をRubyで表現すると、およそ以下と同等らしい。(非常に分かり易いコードの抜粋に感謝です!)
include を Ruby で書くと以下のように定義できます。
def include(*modules) modules.each {|mod| # append_features はプライベートメソッドなので # 直接 mod.append_features(self) とは書けない mod.__send__ :append_features, self # 1.7 以降は以下の行も実行される mod.__send__ :included, self } endhttp://www.ruby-lang.org/ja/man/html/Module.html#append_features
Module#includeとObject#extendの実装は実はおよそ以下の通りになっています。
class Module def include(*modules) raise if modules.any?{|mod| mod.instance_of?(Module)} modules.reverse_each |mod| mod.append_features(self) mod.included(self) end end def append_features(class_or_mod) include_module(class_or_mod) end def included(class_or_mod) end def extend_object(object) include_module(object.singleton_method) end def extended(object) end # includeの本体、rb_include_module # 実際にはこのメソッドはRubyから直接触れることはできない def include_module(class_or_mod) class_or_modの継承チェインにselfとselfにincludeされている モジュールを追加する ただしすでにincludeされているものは無視される end end class Object def extend(*modules) raise if modules.any?{|mod| mod.instance_of?(Module)} modules.reverse_each |mod| mod.extend_object(self) mod.extended(self) end end endhttp://www.kmc.gr.jp/~ohai/diary/?date=20060820
- つまり、includeの実態はappend_featuresでinclude_moduleが実行されることなのだが、実はその後includedも呼び出されていた。
- include本来の目的はappend_featuresで処理され、includedは独自の追加処理を設定したい時に利用する意図があるようだ。
- append_featuresでsuperを設定して以下のように利用するのと同等。(実際、自分では今までこの方法で独自の処理を追加していた。)
def self.append_features(class_or_mod) super # 独自の追加処理 end
- 同じようにextendメソッドに対応するextendedメソッドも存在している。