(Ruby) Module の include で クラスメソッドを追加する方法

Rails や添付ライブラリ等で利用されているテクニック。

何かのクラスにある性質を追加したい場合、モジュールの include を行うわけです(代表例:include Enumberable)。

素直にモジュール定義をして、クラス定義側で include するだけだと、インスタンスメソッド等はクラス側に追加されますが、include したクラスのクラスメソッドを追加することはできません。

でもこれでは足りなくて、クラスメソッドを拡張したい場合があります。これは Rubyメタプログラミング機能を利用して可能です。

実際に、ActiveRecordの基礎クラスである ActiveRecord::Base には最低限のクラスメソッド、インスタンスメソッドしか定義されていません。バリデーションに利用されている validates_length_of 等のクラスメソッドは ActiveRecord::Validations 内に定義されています。具体的には Module#included 等を利用して実現しています。

実際の例

以下は ActiveRecord::Validations モジュールの実際のコードです。

  def self.included(base) # :nodoc:
    base.extend ClassMethods
    base.class_eval do
      alias_method_chain :save, :validation
      alias_method_chain :save!, :validation
      alias_method_chain :update_attribute, :validation_skipping
    end
  
    base.send :include, ActiveSupport::Callbacks
    base.define_callbacks *VALIDATIONS
  end

以下、主要な部分だけ開設します。

self.included(base)

Module#included で、モジュールがインクルードされたときに呼ばれるメソッドです。引数にはインクルードしたモジュールまたはクラスが入ります。
ここでは、 base に ActiveRecord::Base が入ります。

base.extend ClassMethods

実際の拡張はここで行っています。

extend は引数に渡されたモジュールに定義されたインスタンスメソッドをレシーバの特異メソッドとして拡張します。 ClassMethods は ActiveRecord::Validations 内にされたモジュールです(ActiveRecord::Validations::ClassMethods)。ClassMethods の中に、先に触れた validates_length_of 等の定義があります。

ここでは base (= ActiveRecord::Base) に対して特異メソッドを追加していますから、実際に拡張されるのはクラスオブジェクトです。クラスオブジェクトのインスタンスメソッドはつまりクラスメソッドですから、クラスメソッドが拡張されています。

その他

base.class_eval を利用して save に validation 機能をつけたり等しています。alias_method は実際にメソッドが定義されていないとエラーになるため、include 後のここで実行する必要があります。ActiveRecord::Base 側の include 後に書いてもいいですが、編集しづらいし、なにより判り辛いです。

base.send :include, ActiveSupport::Callbacks の部分ですが、ここは何でこうしてるか判りません…。class_eval の中じゃだめなのかなぁ? わかる人いたら教えてください(ぉ

rdoc の見方

自分はこれで盛大にハマったのですが、こういうわけで、ActiveRecordのクラス/インスタンスメソッドを調べようと思ったら - 例えば今回の Validations 関連なら少なくとも ActiveRecord::Validations、ActiveRecord::Validations::ClassMethods の二つは見る必要があります。

Rails のコードの書き方はこういった形で統一されているので、覚えてしまえば rdoc は却ってほかより読みやすいのかもしれません。