投稿日:2026/8/5
更新日:2026/8/5

モデルからバリデーションを別モジュールへ切り出すと、だいたい先頭に extend ActiveSupport::Concern が付いてくる。これが何をしているのか、なぜ include ではなく extend なのかを整理しておく。
extend ActiveSupport::Concern は 「このモジュールで included do / class_methods do を使えるようにする」宣言。included do のブロックは include 先のクラスのコンテキストで評価される。だから validates のようなクラスマクロが書ける。self.included(base) + base.class_eval の定型句を短くしただけで、やっていることは同じ。extend はモジュール自身の能力拡張、include はクラスへの機能取り込み。主語が違う。validates は クラスメソッド。素の module にそのまま書いても動かない。
module Article::Validators
validates :title, presence: true # → NoMethodError
end
module 自身に対して validates を呼んだことになるからだ。モジュールは ActiveRecord::Base ではないので、そんなメソッドは持っていない。
呼びたい相手は「モジュール」ではなく「include したクラス」。この主語のズレが、
included doが存在する理由のすべて。
Ruby には「include された瞬間に呼ばれるフック」self.included がある。これを使えば Concern なしでも書ける。
module Article::Validators
def self.included(base) # base = include したクラス(= Article)
base.class_eval do # そのクラスのコンテキストで評価する
validates :title, presence: true
end
end
end
動くが、モジュールを切り出すたびに self.included(base) + base.class_eval という定型句を毎回書くことになる。
module Article::Validators
extend ActiveSupport::Concern
included do
validates :title, presence: true
end
end
class Article < ApplicationRecord
include Validators
end
included do ... end が上の定型句を肩代わりする。ブロックの中身は include したクラスのコンテキストで評価されるので、validates は Article に対して呼ばれた形になる。
include したときに何が起きているかを追うとこうなる。
sequenceDiagram
participant A as Articleクラス
participant V as Validatorsモジュール
participant C as Concernの仕組み
A->>V: include Validators
V->>C: included フックが発火
C->>A: included do のブロックを class_eval
Note over A: validates がクラスに対して呼ばれる
ここが一番混乱しやすい。1つの機能を作るのに、2種類の取り込みが登場する。
module Article::Validators
extend ActiveSupport::Concern # ← モジュール自身の能力を拡張する
# ...
end
class Article < ApplicationRecord
include Validators # ← クラスに機能を取り込む
end
flowchart TD
AS["ActiveSupport::Concern"] -->|extend| V["Article::Validators<br/>モジュール"]
V -->|include| A["Article<br/>モデルクラス"]
AS -.->|"included do / class_methods do<br/>が書けるようになる"| V
V -.->|"validates がクラスに適用される"| A
| 誰に対して | 何が起きるか | |
|---|---|---|
extend M |
そのオブジェクト自身(ここでは Validators モジュール) |
特異メソッドとして生える。included / class_methods という DSL が Validators で使えるようになる |
include M |
そのクラスのインスタンス | インスタンスメソッドとして取り込まれる |
Validators は「included do という書き方を使えるようになりたい」側なので extend。モデルは「バリデーションを取り込みたい」側なので include。
included do と並ぶもう1つの主要機能。include 先にクラスメソッドを生やしたいときに使う。
module Searchable
extend ActiveSupport::Concern
class_methods do
def search_by_title(keyword)
where('title LIKE ?', "%#{keyword}%")
end
end
end
Article.search_by_title('Rails') # → 使える
素の Ruby だと module ClassMethods ... end を定義して base.extend(ClassMethods) する必要がある。これも定型句の省略にすぎない。
# class_methods do と等価な素の書き方
module Searchable
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def search_by_title(keyword) = where('title LIKE ?', "%#{keyword}%")
end
end
Concern が単なる糖衣構文ではない部分がここ。モジュールが別のモジュールに依存する場合、素の Ruby だと included フックが正しく伝播しない。
module Timestampable
extend ActiveSupport::Concern
included { scope :recent, -> { order(created_at: :desc) } }
end
module Publishable
extend ActiveSupport::Concern
include Timestampable # Concern 同士のネスト
included { scope :published, -> { where.not(published_at: nil) } }
end
class Article < ApplicationRecord
include Publishable # recent も published も両方使える
end
素の module でこれをやると、Publishable を include したクラスに対して Timestampable の included フックが発火しない(Timestampable は Publishable に include された時点でフックを消費してしまう)というのが古典的なハマりどころだった。ActiveSupport::Concern は依存関係を記録しておき、最終的に「本物のクラス」へ include されたタイミングでまとめて解決する。
ただし、単にバリデーションを1階層切り出すだけのケースではこの機能は使っていない。実質「
included doを使うための宣言」と読んで差し支えない。
included do の中は「クラスのコンテキスト」。self はモジュールではなく include 先のクラスを指す。定数やインスタンス変数の解決先を勘違いしやすい。included do の外に書く。中に書くと class_eval 経由で定義されてしまい、モジュール経由の上書き(super での呼び出し)が効かなくなる。module Article::Validators
extend ActiveSupport::Concern
included do
validates :title, presence: true # クラスマクロ → included do の中
end
def title_present? # インスタンスメソッド → 外
title.present?
end
end
include する側のファイルで名前空間を省略しすぎない。Article::Validators を Article 内から include Validators と書けるのは Ruby の定数探索のおかげだが、別クラスから include するときはフルパスが要る。