includeする時にクラスメソッドの呼び出しと定義を行うmodule書式

今までrestful_authenticationは以下のURLを指定してインストールしていたのだが...

script/plugin install http://svn.techno-weenie.net/projects/plugins/restful_authentication/

ふと思い立って、プラグイン名だけ指定してインストールコマンドを実行してみた。(実は以前も試していて、その時はいつまで経っても反応がなく、諦めていたのだった。)

script/plugin install restful_authentication

すると、問題もなく正常にインストールされてしまった。
中身を確認してみると、何と!今までよりも進化洗練されているようだ。

  • パスワードを保存する時のセキュリティが、より安全になったようだ。
  • モジュールが階層化され、目的毎に分類されている。

ソースコードを読んでみて、早速立ち止まってしまったのがモジュールの書き方。例えば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
  }
end
http://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の継承チェインにselfselfincludeされている
    モジュールを追加する
    ただしすでに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
end
http://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メソッドも存在している。