1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Active Support Concernモジュール ― メタプログラミングRubyで読み解くConcernの仕組み

1
Last updated at Posted at 2026-03-29

Active Support Concernモジュール

概要

この記事は、「メタプログラミングRuby 第2版」第10章「Active Supportのconcernモジュール」を読み学習した内容を個人学習用にまとめ直したものです。

RailsのActive Supportが提供するActiveSupport::Concernは、モジュールのミックスインにまつわる煩雑さを解消するための仕組みです。本記事では、メタプログラミングの観点から、Concernモジュールがどのようにクラスメソッドの追加やモジュール依存関係の解決を実現しているのかを読み解きます。

includeとextendのイディオム

Rails 2 の頃は、妥当性確認メソッドは以下のように全てがActiveRecord::Validationsで定義されていた(Active Modelライブラリは当時存在しなかった)。

module ActiveRecord
  module Validations
    # ...

    # モジュールがインクルードされた時に発火するフックメソッド
    # 内部モジュールのClassMethodsを同時にエクステンドする
    def self.included(base)
      base.extend ClassMethods # baseはインクルード先のクラス
      # ...
    end

    module ClassMethods
      def validates_length_of(*attrs) # ...
        # ...
      end

      def valid?
        # ...
      end

      # ...
    end
  end
end

ActiveRecord::BaseValidationsをインクルードすると、以下のような流れでインスタンスメソッドとクラスメソッドの両方を手にいれる。

  1. Validationsのインスタンスメソッド(valid?など)が、Baseクラスのインスタンスメソッドになる

  2. RubyがActiveRecord::Baseを引数にして、Validationsincludedというフックメソッドを呼び出す

  3. フックメソッドの中で、BaseクラスをActiveRecord::Validations::ClassMethodsでエクステンド(クラス拡張)し、ClassMethodsのメソッドはBaseのクラスメソッドとなる

includeの連鎖の問題

上記のイディオムはインスタンスメソッドとクラスメソッドを同時に挿入できるという意味では便利ではあるものの、以下のような連鎖したインクルード時に問題が発生する。

module SecondLevelModule
  def self.included(base)
    base.extend ClassMethods
  end

  def second_level_instance_method
    "ok"
  end

  module ClassMethods
    def second_level_class_method
      "ok"
    end
  end
end

module FirstLevelModule
  def self.included(base)
    base.extend ClassMethods
  end

  def first_level_instance_method
    "ok"
  end

  module ClassMethods
    def first_level_class_method
      "ok"
    end
  end

  include SecondLevelModule
end

class BaseClass
  # 内部でSecondLevelModuleをインクルードしたモジュールをさらにインクルード
  include FirstLevelModule
end

この形では以下のように、インスタンスメソッドに関しては期待通り全てがBaseへ挿入される。

BaseClass.new.first_level_instance_method # => "ok"
BaseClass.new.second_level_instance_method # => "ok"

しかし、クラスメソッドはFirstLevelModule::ClassMethodsに関しては期待通りにBaseのクラスメソッドとなっているが、SecondLevelModule::ClassMethodsNoMethodErrorとなってしまう。

BaseClass.first_level_class_method # => "ok"
BaseClass.second_level_class_method # => "NoMethodError"

これは、RubyがSecondLevelModuleを呼び出すタイミングにおいては、インクルード先を示す変数baseFirstLevelModuleなためである。

つまり、second_level_class_methodFirstLevelModuleオブジェクト自身の特異メソッドとして定義される。includeはインスタンスメソッドを継承チェーンに載せるが、特異メソッドは継承チェーンに載らないため、BaseClassへは伝搬されない。

ActiveSupport::Concernによる解決

ActiveSupport::Concernを使うと、上記の連鎖の問題が解消される。各モジュールでextend ActiveSupport::Concernし、ClassMethods内部モジュールを定義するだけでよい。includedフックを自分で書く必要はなくなる。

require "active_support"

module SecondLevelModule
  extend ActiveSupport::Concern

  def second_level_instance_method
    "ok"
  end

  module ClassMethods
    def second_level_class_method
      "ok"
    end
  end
end

module FirstLevelModule
  extend ActiveSupport::Concern

  include SecondLevelModule

  def first_level_instance_method
    "ok"
  end

  module ClassMethods
    def first_level_class_method
      "ok"
    end
  end
end

class BaseClass
  include FirstLevelModule
end

今度はインスタンスメソッドもクラスメソッドも全てBaseClassに挿入される。

BaseClass.new.first_level_instance_method  # => "ok"
BaseClass.new.second_level_instance_method # => "ok"
BaseClass.first_level_class_method         # => "ok"
BaseClass.second_level_class_method        # => "ok"

ActiveSupport::Concernは、モジュールがインクルードされた際にClassMethodsextendを最終的なインクルード先(BaseClass)に対して実行する。これにより、連鎖の途中で特異メソッドとして行き止まりになる問題が解消される。

Concernの仕組み

Concernの仕組みはextendedappend_featuresの二つの重要なメソッドによって成り立つ。

extended

あるモジュールがextend ActiveSupport::Concernを実行すると、RubyがConcern.extendedフックを呼び出し、そのモジュールに@_dependenciesという空の配列がセットされる。

この配列が、後述するappend_featuresで依存モジュールを管理するために使われる。

module ActiveSupport
  module Concern
    class MultipleIncludedBlocks < StandardError
      def initialize
        super "Cannot define multiple 'included' blocks for a Concern"
      end
    end

    # モジュールがエクステンドされた時に空の配列をセットする
    def self.extended(base)
      base.instance_variable_set(:@_dependencies, [])
    end

    # ...
  end
end

Module#append_features

Module#append_featuresincludedと同様にinclude時に内部的に呼ばれるが、実行順序と役割が異なる。include時の処理順序は以下の通り。

  1. append_features(base) — モジュールをbaseの継承チェーンに追加する(インクルード処理本体)
  2. included(base) — インクルード完了後に呼ばれるフックメソッド(デフォルトは空なのでオーバーライドして挙動をカスタマイズ)

includedはインクルード後の通知用フックに過ぎないため、オーバーライドしてもインクルード自体の挙動は変わらない。

一方、append_featuresをオーバーライドするとインクルード処理そのものを制御できるが、元々の処理内容が存在するので、下記のように何も記述せずにオーバーライドするとモジュールが一切インクルードされなくなる。

module M
  # 本来の処理(super)を呼ばずにオーバーライド
  def self.append_features(base)
  end
end

class C 
  include M 
end

# 継承チェーンにモジュールがインクルードされなくなる
C.ancestors # => [C, Object, Kernel, BasicObject]

Concern#append_features

ActiveSupport::Concernappend_featuresを以下のようにオーバーライドしている。

module ActiveSupport
  module Concern
    def append_features(base)
      if base.instance_variable_defined?(:@_dependencies)
        base.instance_variable_get(:@_dependencies) << self
        false
      else
        # baseがモジュール自身でなく、@_dependenciesが定義されていない場合
        # つまり「最終的なインクルード先のクラス」である場合
        return false if base < self
        @_dependencies.each { |dep| base.include(dep) }
        super
        # ここでClassMethodsのエクステンドをまとめて行う
        base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
        # ...
      end
    end
  end
end

base(インクルード先)が@_dependenciesを持っているかどうかで処理が分岐する。

  • baseがConcernモジュールの場合@_dependenciesを持つので、superを呼ばずに自身を依存として蓄積するだけでインクルードを保留する
  • baseがクラスなど最終的なインクルード先の場合@_dependenciesを持たないので、蓄積した依存モジュールを順にインクルードし、superで自身もインクルード、ClassMethodsextendをまとめて行う

この仕組みにより、連鎖の途中ではインクルードを保留し、最終的なクラスに到達した時点で全てが正しいbaseに対して適用される。

まとめ

  • Rails 2時代のincludedフック + extend ClassMethodsイディオムでは、モジュールが連鎖してインクルードされた場合にクラスメソッドが最終クラスへ伝搬しない問題がある
  • ActiveSupport::Concernextendすることで、ClassMethods内部モジュールのextendが最終的なインクルード先に対して自動的に行われる
  • Concernはextendedフックで@_dependencies配列をセットし、append_featuresをオーバーライドすることで実現されている
  • Concernモジュール間のインクルードでは実際のインクルード処理を保留し、最終クラスに到達した時点で蓄積した依存を順にインクルードする

補足: 現代のRailsにおけるConcernの拡張

本記事は書籍の内容に基づき、Concernの核心である依存解決とClassMethodsの自動extendの仕組みを解説した。現代のRails(8.x時点)では、以下の機能がさらに追加されている。

  • included ブロックincluded do ... end の形でブロックを渡すと、最終的なインクルード先クラスのコンテキストでclass_evalされる。append_features内でClassMethodsextendと合わせて実行される
  • class_methods DSLmodule ClassMethodsを手動定義する代わりに、class_methods do ... endブロックで簡潔に定義できる(Rails 4.2以降)
  • prependサポートprepend_featuresのオーバーライドにより、includeと同様の依存解決がprependでも可能になった(Rails 6.1以降)。本記事の説明はincludeベースの動作を対象としている

参考文献

この記事は以下の情報を参考にして執筆しました。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?