【Rails】include と extend と ActiveSupport::Concern

`include` と `extend` の違いってなんだ？
内部では何が起きているのか..?

`ActiveSupport::Concern` を使うにも `included` 使うけど何をしているのか

`ActiveSupport::Concern`を使う際に学んだことをメモします。
## [extend](https://docs.ruby-lang.org/ja/latest/method/Object/i/extend.html)について

- `extend` したクラスのシングルトンクラス（特異クラス）のインスタンスメソッドとして追加できる
- `extend`したモジュールのメソッドはクラスメソッドのように呼び出せる


順を追うと...

### シングルトンクラスとは...
**あるオブジェクト専用のクラスであり、そのオブジェクトだけに属する**
[こちらが参考になります](https://qiita.com/k-penguin-sato/items/d637dced7af32e4ec7c0)
ちなみに[object.singleton_class](https://docs.ruby-lang.org/ja/latest/method/Object/i/singleton_class.html)で確認することができる

```ruby 
class Bar; end

p Bar.singleton_class # => #<Class:Bar>
bar = Bar.new
p bar.singleton_class # => #<Class:#<Bar:0x0000000102846720>>
```
`Barクラス` は `Classクラス` のオブジェクトなので、 
`Classクラス` のシングルトンクラスが生成されます。

また、「そのオブジェクトのみに属する」という特徴があるため、オブジェクトを別々で生成した場合も、それぞれでシングルトンクラスが生成されます。（ややこしい...）
```ruby
bar = Bar.new
bar_1 = Bar.new

p bar.singleton_class # => #<Class:#<Bar:0x00000001007a9670>>
p bar_1.singleton_class # => #<Class:#<Bar:0x00000001007a95d0>>
```

### 特異メソッドとクラスメソッドの関連性

クラスメソッドは特異メソッドの一種と見なすことができます。

というのも
**クラスメソッドは特異クラスのインスタンスメソッドになるから**です。
実際に確認してみましょう。

```ruby
class Bar
  # クラスメソッドを定義
  def self.good
    p 'いいね!!'
  end
end

p Bar.singleton_class.instance_methods # => [:good,...]
```

上記のように `Bar` のクラスメソッドは、
シングルトンクラスのインスタンスメソッドに定義されていそうです。

### シングルトンメソッドを追加できる
[define_singleton_method](https://docs.ruby-lang.org/ja/latest/method/Object/i/define_singleton_method.html)でシングルトンメソッドを後で追加することができます。

``` ruby
class Bar
  def self.good
    p 'いいね!!'
  end
end

Bar.define_singleton_method :nice do
  p '素敵！'
end

p Bar.singleton_class.instance_methods # => [:nice, :good,...]
p Bar.singleton_methods # => [:nice, :good]
```


### さて、extendについて

>- `extend`したモジュールのメソッドはクラスメソッドのように呼び出せる

上記のように説明できる理由として

クラスメソッドは
**シングルトンクラスのインスタンスメソッド（特異メソッド）であり、**

extendは
**特異メソッドに新しくメソッドを追加できる**

からです。

``` ruby
module Foo
  def hello(name)
    p "こんにちは、#{name}さん"
  end
end

class Bar
  extend Foo
end

# シングルトンクラスに hello メソッドが追加されている
p Bar.singleton_methods  # => [:hello]
# クラスメソッドのように hello メソッドを呼び出す
Bar.hello('太郎')  # => こんにちは、太郎さん

# インスタンスメソッドとして呼び出そうとすると失敗する
# Bar クラスのインスタンスである bar のシングルトンメソッドには hello が定義されていないから...!
bar = Bar.new
p bar.hello  # => NoMethodError: undefined method `hello' for #<Bar:0x...
```

## [include](https://docs.ruby-lang.org/ja/latest/method/Module/i/include.html) について
`include` とは、指定されたモジュールの定義 (メソッド、定数) を引き継ぐことです。
モジュールによる機能追加は、そのクラスとスーパークラスとの継承関係の間にそのモジュールが挿入されることで実現されます。
そのため多重継承の代わりに用いられており、 `Mix-in` とも呼ばれます。

``` ruby 
module Foo
  def hello(name)
    puts "こんにちは、#{name}さん"
  end
end

class Hoge; end

class Bar < Hoge
  include Foo
end

# includeされるクラスとそのスーパークラスとの間に差し込まれる
p Bar.ancestors # => [Bar, Foo, Hoge, Object, Kernel, BasicObject]

# includeしたモジュールのインスタンスメソッドを引き継げる
Bar.new.hello('太郎') # => こんにちは、太郎さん
```

## さて、ActiveSupport::Concernですよ...
[ActiveSupport::Concern](https://api.rubyonrails.org/classes/ActiveSupport/Concern.html)のソースを参照しながら見ていきたいと思います。

### 普通にextend, includeではダメなのか？
こちらの記事に分かりやすく書かれていました。
[[Rails] ActiveSupport::Concern の存在理由](https://qiita.com/castaneai/items/6dc121ce6ff100614f42#mix-in%E3%81%AE%E8%A4%87%E9%9B%91%E3%81%AA%E8%A8%98%E8%BF%B0%E3%82%92%E7%9C%81%E7%95%A5%E3%81%A7%E3%81%8D%E3%82%8B)
>Rubyの mix-in は通常以下のようなルールで記述します
>- 切り出した機能を module として作成
>- 共通メソッドを module 内に記述
>- クラスメソッドや組み込み先クラスの内部処理を module に入れたい場合は特殊なメソッドを使う必要がある
>
>ActiveSupport::Concern はこの3番目にある 特殊なメソッド の記述を簡単にしてくれます。

簡単にいうと、include する側のクラスのコンテキストで実行したい特殊な処理を、スッキリいい感じに書けるようになる。

これに関してはコードを見た方が早いです。

### ActiveSupport::Concern なし
- `base` はこのモジュールが取り込まれる先のクラスになる。[こちらを参照](https://docs.ruby-lang.org/ja/latest/method/Module/i/included.html)
- `extend`は言わずもがな。modele内で定義したクラスメソッドを、`include` 先のシングルトンメソッドとして追加している。
- `class_eval`については[こちらの記事](https://qiita.com/y4u0t2a1r0/items/34c73d131b840c27c116)を参照されたい。要するにメタプログラミング的に、`base` のインスタンスメソッドにメソッドを追加することでbaseクラスのコンテキストでメソッドを実行できるようになる。
- `self.included(base)`については、`module` に `included メソッド` を定義しておくと、`module` が `include` された時に実行されるようになる。[こちらを参照](https://www.techscore.com/blog/2013/03/01/rails-include%E3%81%95%E3%82%8C%E3%81%9F%E6%99%82%E3%81%AB%E3%82%AF%E3%83%A9%E3%82%B9%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%A8%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E3%83%A1/)



```ruby
module M
  def self.included(base)
    base.extend ClassMethods
    base.class_eval do
      scope :disabled, -> { where(disabled: true) }
    end
  end

  module ClassMethods
    ...
  end
end
```


ちなみに、
`concern` で追加したインスタンスメソッドと、追加先のクラスのインスタンスメソッドの命名がバッティングした場合は、追加先のクラスのインスタンスメソッドが優先的に実行される。

```ruby
module M
  def self.included(base)
    base.class_eval do
      def example_method
        p "追加されたメソッドです"
      end
    end
  end
end

class MyClass
  include M
  def example_method
    p '元々あったメソッドです'
  end
end

MyClass.new.example_method # => '元々あったメソッドです'
```
というのも`include`された`module`は継承関係的に上位に来るので、命名が被った場合は小クラスのメソッドが優先的に実行されるからです。
```ruby
p MyClass.ancestors #=> [MyClass, M, Object, Kernel, BasicObject]
```


### ActiveSupport::Concern あり
- `include` 先のコンテキストで実行したいものは `include do; end`内に記述
- クラスメソッド的に使用したいものは `class_method` に記述
```ruby
module M
  extend ActiveSupport::Concern

  included do
    scope :disabled, -> { where(disabled: true) }
  end

  class_methods do
    ...
  end
end
```
シンプルな書き方でいい感じに処理できるようになりました。🎉


他にも **依存関係の解消までやってくれる** などの利点があるのだが、
ここでは触れないでおく。



### 内部ではどんな処理をしているのか

[concern](https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/activesupport/lib/active_support/concern.rb)
```ruby
module Concern
    class MultipleIncludedBlocks < StandardError #:nodoc:
      def initialize
        super "Cannot define multiple 'included' blocks for a Concern"
      end
    end

    class MultiplePrependBlocks < StandardError #:nodoc:
      def initialize
        super "Cannot define multiple 'prepended' blocks for a Concern"
      end
    end

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

    def append_features(base) #:nodoc:
      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

    def prepend_features(base) #:nodoc:
      if base.instance_variable_defined?(:@_dependencies)
        base.instance_variable_get(:@_dependencies).unshift self
        false
      else
        return false if base < self
        @_dependencies.each { |dep| base.prepend(dep) }
        super
        base.singleton_class.prepend const_get(:ClassMethods) if const_defined?(:ClassMethods)
        base.class_eval(&@_prepended_block) if instance_variable_defined?(:@_prepended_block)
      end
    end

    def included(base = nil, &block)
      if base.nil?
        if instance_variable_defined?(:@_included_block)
          if @_included_block.source_location != block.source_location
            raise MultipleIncludedBlocks
          end
        else
          @_included_block = block
        end
      else
        super
      end
    end

    def prepended(base = nil, &block)
      if base.nil?
        if instance_variable_defined?(:@_prepended_block)
          if @_prepended_block.source_location != block.source_location
            raise MultiplePrependBlocks
          end
        else
          @_prepended_block = block
        end
      else
        super
      end
    end

    def class_methods(&class_methods_module_definition)
      mod = const_defined?(:ClassMethods, false) ?
        const_get(:ClassMethods) :
        const_set(:ClassMethods, Module.new)

      mod.module_eval(&class_methods_module_definition)
    end
  end
end
```

はい、意味がわかりませんね😭
一つずつ噛み砕いていく

### self.extended(base)

モジュールを`extend`した時に呼ばれ、`extended` を呼び出してインスタンス変数である`@_dependencies`をセット。
```ruby
    def self.extended(base) #:nodoc:
      base.instance_variable_set(:@_dependencies, [])
    end
```

実際に使われる、`included`, `class_methods`について見てみる

### included
```ruby
    def included(base = nil, &block)
      if base.nil?
        if instance_variable_defined?(:@_included_block)
          if @_included_block.source_location != block.source_location
            raise MultipleIncludedBlocks
          end
        else
          @_included_block = block
        end
      else
        super
      end
    end
```
`base` の初期値は `nil` が確定
続いて `if instance_variable_defined?(:@_included_block)` では、初回実行時は`false` が確定なので `@_included_block = block` が実行される。

### class_methods

```ruby
def class_methods(&class_methods_module_definition)
    mod = const_defined?(:ClassMethods, false) ?
    const_get(:ClassMethods) :
    const_set(:ClassMethods, Module.new)

    mod.module_eval(&class_methods_module_definition)
end
```

[const_defined?(:ClassMethods, false)](https://docs.ruby-lang.org/ja/latest/method/Module/i/const_defined=3f.html) で、 `ClassMethods` という名前の `module` が存在するかを確認する。
（falseオプションで継承関係を無視してそのクラス内のみで確認する）

もし、見つかればそれを取得して、なければ `module` として定義する。

その後、[module_eval](https://docs.ruby-lang.org/ja/latest/method/Module/i/class_eval.html) によって `ClassMethods` のインスタンスメソッドとしてブロック引数である `&class_methods_module_definition` を追加している。

つまり
```ruby
module Sample
  extend ActiveSupport::Concern

  class_methods do
    def class_method
      p 'クラスメソッドです'
    end
  end
end
```
これが
↓
こうなっているのと同じ状態
```ruby
module Sample
  module ClassMethods
    def class_method
      'クラスメソッドです'
    end
  end
end
```
`ClassMethods`モジュールを作成し、その内部には引数で受けたメソッドがインスタンスメソッドとして定義される。

### 閑話休題

現状
**`included`** : `@_dependencies`をセットする
**`class_methods`** : `ClassMethods` モジュールを作成し、ブロックで指定したメソッドを持たせる

この段階では `include` されるクラスにクラスメソッドを追加したり、`include` される側のコンテキストで処理を実行することはできない。

そこで、重要となってくるのが `append_features` メソッドらしい。


### [append_features](https://docs.ruby-lang.org/ja/2.1.0/method/Module/i/append_features.html)

>このメソッドは Module#include の実体であり、...

と説明がある通り、`included`された時に呼ばれることがわかります
試してみる🧐
```ruby
module M
  def self.append_features(_base)
    p 'includeされた'
  end
end

class MyClass; include M; end

MyClass.new => # 'includeされた'
```
確かに`include`される時に実行される。

つまり、 `ActiveSupport::Concern` を `extend` したモジュールを `include` すると、内部で実行されるはずの `append_features メソッド` を独自のメソッドでオーバーライドしているようだ。

```ruby
    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
```
- `base` とは `include` される側のクラスになる。

- `if base.instance_variable_defined?(:@_dependencies)` の条件分岐では、
`include`される側のクラスが `concern` でない場合、`@_dependencies`がないため`false`になる。

- **`super`**
によって既存の `append_features` が継承されモジュールが `include` される。
ちなみに `super` がないと、`include` できないようだ（[こちらを参照](https://qiita.com/kanpou0108/items/ce172d955a87e7b0c756))


- **`base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)`**
によって
`ClassMethods` モジュールを `extend` することで `ClassMethods` に定義されたメソッドを `base` クラスのクラスメソッドとして追加することができる。

- **`base.class_eval(&@_included_block)`** 
によって
`class_eval` により `base` クラスのコンテキストで `@_included_block` を実行できるようになる。

以上、`included`, `class_methods`, `append_features`によって`ActiveSupport::Concern`の大枠の仕組みが実現できているようだ。

難しい(;´д｀)

## 余談

[prepended](https://docs.ruby-lang.org/ja/latest/method/Module/i/prepended.html) は `Module#prepend` されたときに実行されるので、今回はスルーします。
[prepend_features](https://docs.ruby-lang.org/ja/latest/method/Module/i/prepend_features.html) は `Module#prepend` から呼び出されるメソッドで、 `prepend` の処理の実体であるため、こちらも同様にスルーします。

## 最後に
ここまで見ていただきありがとうございます。m(._.)m

`extend` や `include`の内部挙動について知れたのは個人的に良かったです。

`ActiveSupport::Concern`が開発された背景である **「モジュールの依存関係を解消する」** については今回ノータッチだったので、別の機会に勉強できたらと思います。


 ## 参考
 - [【Ruby】特異メソッドについて](https://qiita.com/ta-chibana/items/7965d246ab67e14c175a)
- [一歩ずつ理解する特異メソッドと特異クラス](https://qiita.com/kyntk/items/0d53c86e2d4d9c4d3456)
- [ActiveSupport::Concernが裏でやっていること](https://mogulla3.tech/articles/2019-05-06-01/)
- [instance_evalとclass_evalについての備忘録](https://qiita.com/y4u0t2a1r0/items/34c73d131b840c27c116)
- [Rails: includeされた時にクラスメソッドとインスタンスメソッドを同時に追加する頻出パターン](https://www.techscore.com/blog/2013/03/01/rails-include%E3%81%95%E3%82%8C%E3%81%9F%E6%99%82%E3%81%AB%E3%82%AF%E3%83%A9%E3%82%B9%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%A8%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E3%83%A1/)
- [【Rails】ActiveSupport::Concernのしくみについて](https://sakaishun.com/2021/10/17/activesupport-concern/)
- [ActiveSupport::Concernが裏でやっていること](https://mogulla3.tech/articles/2019-05-06-01/)
- [Module#append_featuresを上書きする時はsuperを書かないとモジュールがincludeされない](https://qiita.com/kanpou0108/items/ce172d955a87e7b0c756)