【悲報】ActiveSupport::Concern の ClassMethod はモジュールメソッドになる件

皆さんこんにちは!「太鼓式マッサージ?面白そう!」と思ったら「タイ古式マッサージ」でした。MUGENUP の osada です。名前って難しいですよね。

ところで、ActiveSupport::ConcernClassMethod で定義したクラス変数が、どこにあるか、ご存知ですか?

# 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 だからではないようですね。違いは、includedClassMethodなので、期待通りに動かないのはClassMethodのせいのようです。

ClassMethodincludedの違いはどこにあるのか?

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

f:id:mgnup:20131118023234p:plain