【悲報】ActiveSupport::Concern の ClassMethod はモジュールメソッドになる件
皆さんこんにちは!「太鼓式マッサージ?面白そう!」と思ったら「タイ古式マッサージ」でした。MUGENUP の osada です。名前って難しいですよね。
ところで、ActiveSupport::Concern
の ClassMethod
で定義したクラス変数が、どこにあるか、ご存知ですか?
# app/models/concern/item_module.rb module ItemModule extend ActiveSupport::Concern module ClassMethods def my_module_method @@concern_class_variable ||= "concern_class_variable" end end end
# app/models/item.rb class Item < ActiveRecord::Base include ItemModule end
# spec/models/item_spec.rb describe Item do it "ClassMethods で定義すると参照できる" do expect(Item.my_module_method).to eq "concern_class_variable" end end
ここまではOKだと思います。 では、これを自身のクラス変数として参照できないことをご存知でしょうか?
it "ClassMethods で取り込んだクラスメソッドを実行しても自身のクラス変数としては定義されない" do expect{ Item.my_module_method }.not_to change{Item.class_variable_defined?(:@@concern_class_variable)}.from(false) end
参照はできるのに、定義されていない。それは一体どこにあるの?というのが今回のお話です。
クラス変数はどこに定義されているの?
ruby の場合、クラスメソッドはクラスに特異メソッドを追加する、という文脈で定義されます。
Ruby におけるクラスメソッドとはクラスの特異メソッドのことです。 したがって、何らかの方法でクラスオブジェクトにメソッドを定義すれば、そ れがクラスメソッドとなります。 http://docs.ruby-lang.org/ja/2.0.0/doc/spec=2fdef.html#class_method
よって、クラスメソッド内で定義されたクラス変数は、自身のクラス変数となります。
class Item < ActiveRecord::Base include ItemModule class << self def my_class_methods @@my_class_variable = "my_class_variable" end end end
it "クラスメソッドを実行するとクラス変数が定義される" do expect{ Item.my_class_methods }.to change{Item.class_variable_defined?(:@@my_class_variable)}.from(false).to(true) end
これは全く期待通りの動きです。先ほどと違うのは、自身のクラスメソッドとして定義しているという点ですね。 では、class定義ならOKで、module だからダメなんでしょうか?
module の included として定義してみる
ActiveSupport::Concern
では included
というメソッドで、クラス定義の中でコードを評価することができます。
module ItemModule extend ActiveSupport::Concern included do class << self def my_singleton_module_method @@singleton_include_module_method = "singleton_include_module_method" end end end end
この結果はクラス変数はクラスに紐付きました。期待通りです。
it "included で 拡張した特異クラスのクラスメソッドはクラス変数が定義される" do expect{ Item.my_singleton_module_method }.to change{Item.class_variable_defined?(:@@singleton_include_module_method)}.from(false).to(true) end
どうやら、module だからではないようですね。違いは、included
とClassMethod
なので、期待通りに動かないのはClassMethod
のせいのようです。
ClassMethod
とincluded
の違いはどこにあるのか?
ClassMethod
はどのように動くのでしょうか?
base.extend const_get("ClassMethods") if const_defined?("ClassMethods")
ClassMethod
という定義があったら、base
に対してextend
するという実装になっています。
一方includes
は、
base.class_eval(&@_included_block) if instance_variable_defined?("@_included_block")
def included(base = nil, &block) if base.nil? @_included_block = block else super end end
base クラスのclass_eval
を呼び出しているため、自身のコンテキストで評価されます。
違いはClassMethod
はモジュールのextend
であり、included
はクラス定義内での評価というところです。
引数で指定したモジュールのインスタンスメソッドを self の特異 メソッドとして追加します。 instance method Object#extend
extend されたクラス変数はモジュール変数となる
extend
はモジュールのメソッドを、特異クラスのメソッドとして追加するだけです。よって、extend
で追加されたクラスメソッド内で定義されたクラス変数は、モジュール変数になってしまうのです!!!
it "ClassMethods で定義したクラスメソッドは、モジュール変数" do expect{ Item.my_module_method }.to change{ItemModule::ClassMethods.class_variable_defined?(:@@concern_class_variable)}.from(false).to(true) end
そしてさらに恐ろしいことに、モジュール変数はinclude
した全てで共有されます。マニュアルにしっかり書かれています。
モジュールで定義されたクラス変数(モジュール変数)は、そのモジュールをイ ンクルードしたクラス間でも共有されます。 http://docs.ruby-lang.org/ja/2.0.0/doc/spec=2fvariables.html#class
module Foo @@foo = 1 end class Bar include Foo p @@foo += 1 # => 2 end class Baz include Foo p @@foo += 1 # => 3 end
ClassMethod はモジュールメソッドだ
名前って難しいですよね。ClassMethod
だと思っていたら(そして実際にクラスメソッドなのですが)、その中で定義したクラス変数は、モジュール変数になってしまうのです。
これからはできるだけ ClassMethod
は使わないようにしよう、と心に決めた日でした。
[注意] モジュール変数、クラス変数、クラスメソッドの役割について
モジュール変数を定義するべきではない、というのはよく言われていることだと思います。そもそもモジュールは数学的な意味で関数であるべきだというのが私の持論です。副作用がないと言い換えても良いです。
なので、モジュール内でクラス変数を使うべきではないというご指摘は最もだと思います。クラス変数を使わない方が良いというのも納得できます。
本記事は、クラス変数やクラスメソッドの使用を推奨したものではありません。あくまで、知っておくと罠が回避できるかも、という記事ですので、ご了承のほどよろしくお願い致します。
参考
» ActiveSupport::Concern でハッピーなモジュールライフを送る TECHSCORE BLOG
宣伝
MUGENUP では、rails を使いたいエンジニアを募集中です。 無限流開発、ご一緒しませんか?
大きな裁量で自社サービス開発!Rubyエンジニアをウォンテッド! - 株式会社MUGENUPの求人 - Wantedly