Help us understand the problem. What is going on with this article?

ActiveSupport::Concernの使い方としくみ

More than 1 year has passed since last update.

はじめに

Railsのソースコードを読んでいると頻繁に出くわすActiveSupport::Concernモジュールについて、その使い方とConcernモジュールで行っていることをまとめました。
対象のバージョンは、rails5.2.1です。

記事の目的

railsのソースコードを読むために学習した内容を整理してみました。
最近railsの勉強を始めた初心者なのですが、私と同じような初心者の方の学習の助けになったら嬉しく思います。
また、一次情報にあたるなどしてなるべく間違いの無いようにしたつもりなのですが、もしも記事の内容に間違いがありましたら、大変お手数ですがご教示頂けますと嬉しいです。

ActiveSupport::Concernモジュールとは

RubyonRailsのコンポーネントであり、Ruby言語の拡張などを行っているActiveSupportの一部。
モジュールをインクルードした際に、インスタンスメソッドと一緒にクラスメソッドもインクルーダーに追加する機能をカプセル化したもの。1
あらかじめモジュールにActiveSupport::Concernをエクステンドしておくことによって、クラスメソッドの追加機能を簡単に使えるようになる。
Railsガイド ActiveSupport

ActiveSupport::Concernモジュールの使い方

my_concern.rb
require 'active_support'

module MyConcern
  #モジュールの中でActiveSupport::Concernモジュールをエクステンドする
  extend ActiveSupport::Concern

  def my_instance_method
    "my_instance_method()"
  end

  #ClassMethodsモジュールの中で、インクルーダーに追加するクラスメソッドを定義する
  module ClassMethods
    def my_class_method
      "my_class_method()"
    end
  end
end

class MyClass
  #ActiveSupport::Concernをエクステンドしたモジュールをインクルードする
  include MyConcern
end


obj = MyClass.new
obj.my_instance_method  #=> "my_instance_method()" 

#MyClassに対してMyConcern::ClassMethodsで定義したメソッドを呼び出す
MyClass.my_class_method #=> "my_class_method()" #インクルーダーにクラスメソッドとして追加できている!

このように、ActiveSupport::Concernをエクステンドしたモジュールの中では、ClassMethodsモジュールのスコープ内で定義されたメソッドをインクルーダーのクラスメソッドとして追加することができるようになる。

ActiveSupport::Concernモジュールの仕組み

ソースコードを読んでみる

rails/activesupport/lib/active_support/concern.rb
module ActiveSupport
  module Concern
    class MultipleIncludedBlocks < StandardError #:nodoc:
      def initialize
        super "Cannot define multiple 'included' blocks for a Concern"
      end
    end

    def self.extended(base) #:nodoc:
      base.instance_variable_set(:@_dependencies, [])
    end

    def append_features(base)
      if base.instance_variable_defined?(:@_dependencies)
        base.instance_variable_get(:@_dependencies) << self
        false
      else
        return false if base < self
        @_dependencies.each { |dep| base.include(dep) }
        super
        base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
        base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
      end
    end

  #...以下省略

  end
end

https://github.com/rails/rails/blob/master/activesupport/lib/active_support/concern.rb

ActiveSupport::Concernモジュールのソースコードを理解するためには、extendedメソッドとappend_featuresメソッドについて詳しい定義を知る必要がある。

2つのメソッドについての説明

extendedメソッド
モジュールがクラス(またはモジュール)にエクステンドされた時に呼び出されるフックメソッドである。

module A
  def self.extended(mod)
    puts "#{self} extended in #{mod}"
  end
end
module Enumerable
  extend A
end
 # => prints "A extended in Enumerable"

引用 : https://ruby-doc.org/core-2.5.1/Module.html#method-i-extended

append_featuresメソッド
Rubyで元々用意されているメソッド(コアメソッド)であり、モジュールをインクルードした時にRubyが呼び出す。
インクルードされたモジュールがインクルーダーの継承チェーンに含まれているかどうかを確認し、含まれていなければ継承チェーンにモジュールを追加する機能を持っている(つまり、インクルード機能の本体を受け持っている)。
そのため、append_featuresをオーバーライドするとモジュールが一切インクルードされなくなる
尚、プライベートメソッドのため直接呼び出すことはできない。

参考:
https://github.com/ruby/ruby/blob/trunk/eval.c#L1204-L1221
https://ruby-doc.org/core-2.5.1/Module.html#method-i-append_features
https://ruby-doc.org/core-2.5.1/Module.html#method-i-include

以上に示した2つのメソッドの定義を踏まえ、実際にActiveSupport::Concernを使用する際の手続きに沿ってモジュールのソースコードを読んでみる。
また、メタプログラミングRubyに習って、ActiveSupport::Concernをエクステンドしているモジュールを便宜的に小文字でconcernと表記する。

ActiveSupport::Concernをモジュールへエクステンドする

まず、ActiveSupport::Concernモジュールをモジュールへエクステンドする。
その際に、ActiveSupport::Concernモジュールは、フックメソッドであるextendedを呼び出し、エクステンダー(base)にクラスインスタンス変数@_dependenciesを定義する。(@_dependenciesには、最初は空の配列が入っている。)
ここまでで、全てのconcernはクラスインスタンス変数@_dependenciesと、ActiveSupport::Concernにオーバーライドされたappend_featuresメソッドを持つことが分かる。

concernをモジュールやクラスにエクステンドする

concernをインクルードする際Rubyがappend_featuresメソッドを呼び出す。
ここではオーバーライドされたappend_featuresメソッドが呼び出され、インクルーダー(base)がconcernかどうかを確認している。

インクルーダーがconcernの場合

もしもインクルーダーが@_dependenciesを持っているならば、そのインクルーダーはconcernであるとわかる。
すでにAtiveSupport::Concernをエクステンドしているモジュールに、同じくActiveSupport::Concernをエクステンドしているモジュールをインクルードすると、クラスメソッドを読み込むべきクラスがずれてしまうため、この場合はインクルーダーの継承チェーンに自身を追加する代わりに、@_dependenciesに自身を追加し、依存管理のリストとして保存しておく。

インクルーダーがconcernでは無い場合

すでに他のconcernがインクルーダーにインクルードされていることなどによって、インクルーダーの継承チェーンに自身が追加されていないか確認している。(base < self)
すでに自身が継承チェーンに追加されていれば、二重にインクルードすることを避けるためにfalseを返す。
継承チェーンに追加されていなければ、インクルーダーに依存関係を再帰的にインクルードする。(全ての依存関係をインクルーダーに一気に流し込む)
@_dependenciesに入っている全てのconcernをインクルーダーの継承チェーンに追加した後、superで素のappend_featuresメソッドを呼び出して自分自身を継承チェーンに追加し、インクルーダーにClassMethodsモジュールをエクステンドさせる。

以上のように、ActiveSupport::Concernは依存関係をうまく管理しながら、モジュールにクラスメソッドの読み込みを行わせている。

まとめ

ActiveSupport::Concernのしくみという題名なのに全然うまく説明できてない感がすごいので、特に依存関係を再帰的にインクルードするところなどをうまく説明できるようにしたいです。メタプログラミングRubyはすごい。

参考書籍

メタプログラミングRuby


  1. インクルーダー ... インクルード先のモジュール(クラス)という意味で使用しています 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした