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

###はじめに
Railsのソースコードを読んでいると頻繁に出くわすActiveSupport::Concernモジュールについて、その使い方とConcernモジュールで行っていることをまとめました。
対象のバージョンは、rails5.2.1です。
###記事の目的
railsのソースコードを読むために学習した内容を整理してみました。
最近railsの勉強を始めた初心者なのですが、私と同じような初心者の方の学習の助けになったら嬉しく思います。
また、一次情報にあたるなどしてなるべく間違いの無いようにしたつもりなのですが、もしも記事の内容に間違いがありましたら、大変お手数ですがご教示頂けますと嬉しいです。


###ActiveSupport::Concernモジュールとは

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


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

```ruby: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モジュールの仕組み

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

```ruby: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`メソッドについて詳しい定義を知る必要がある。

#####２つのメソッドについての説明

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

```ruby

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



以上に示した２つのメソッドの定義を踏まえ、実際にActiveSupport::Concernを使用する際の手続きに沿ってモジュールのソースコードを読んでみる。
また、[メタプログラミングRuby](https://www.amazon.co.jp/gp/product/4873117437/ref=s9u_simh_gw_i2?ie=UTF8&pd_rd_i=4873117437&pd_rd_r=a1b55111-b98a-11e8-9369-bbb6f35b33bf&pd_rd_w=28V1t&pd_rd_wg=ZUi0S&pf_rd_m=AN1VRQENFRJN5&pf_rd_s=&pf_rd_r=D5W8VCSEAEKX6P3CBC8C&pf_rd_t=36701&pf_rd_p=a3a1572c-f1ee-4418-98a4-c2d0a69c6cb8&pf_rd_i=desktop)に習って、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](https://www.amazon.co.jp/gp/product/4873117437/ref=s9u_simh_gw_i2?ie=UTF8&pd_rd_i=4873117437&pd_rd_r=a1b55111-b98a-11e8-9369-bbb6f35b33bf&pd_rd_w=28V1t&pd_rd_wg=ZUi0S&pf_rd_m=AN1VRQENFRJN5&pf_rd_s=&pf_rd_r=D5W8VCSEAEKX6P3CBC8C&pf_rd_t=36701&pf_rd_p=a3a1572c-f1ee-4418-98a4-c2d0a69c6cb8&pf_rd_i=desktop)

[^1]: インクルーダー ... インクルード先のモジュール（クラス）という意味で使用しています