こんにちは、かつコーチです。
複数のモデルやコントローラに同じようなコードが増えてきたとき、RailsにはConcernという共通化の仕組みが用意されています。
ActiveSupport::Concernを使うと、モジュールの形で関心事を切り出せますが、便利さゆえに乱用してかえって見通しが悪くなるケースも少なくありません。
今回は、Concernの仕組みと実例、そして乱用を避けるための判断基準を解説します。
ActiveSupport::Concernの仕組み
素のRubyモジュールとの違い
Railsを使わない素のRubyでも、モジュールをincludeして共通処理を混ぜ込むことは可能です。
ただし、モジュール側でクラスメソッドやバリデーション、アソシエーションを定義したい場合、includedフックとClassMethodsモジュールを自前で書く必要があり、コードが煩雑になります。
ActiveSupport::Concernは、この定型的な記述を簡潔にまとめるための仕組みです。
# app/models/concerns/archivable.rb
module Archivable
extend ActiveSupport::Concern
included do
scope :archived, -> { where.not(archived_at: nil) }
scope :active, -> { where(archived_at: nil) }
end
class_methods do
def archive_all!
update_all(archived_at: Time.current)
end
end
def archived?
archived_at.present?
end
def archive!
update!(archived_at: Time.current)
end
end
included do ... endの中はクラス本体で実行される扱いになり、scopeやhas_manyなどのクラスマクロをそのまま書けます。
class_methods do ... endで定義したメソッドは、includeしたクラスの特異メソッド(クラスメソッド)として使えるようになります。
モデルへの適用例
「アーカイブ機能」を持つ複数のモデルに、このConcernを適用してみます。
# app/models/article.rb
class Article < ApplicationRecord
include Archivable
end
# app/models/product.rb
class Product < ApplicationRecord
include Archivable
end
article = Article.find(1)
article.archive!
article.archived? # => true
Article.archived # => アーカイブ済みの記事一覧
Article.archive_all! # => 全記事を一括アーカイブ
ArticleとProductという異なるモデルに対して、同じ「アーカイブする」という関心事を1つのモジュールで表現できました。
コントローラでの共通処理の切り出し
Concernはモデルだけでなく、コントローラの共通処理にもよく使われます。
たとえば、複数のコントローラで「ページネーション用のパラメータを組み立てる」処理を共通化する例です。
# app/controllers/concerns/paginatable.rb
module Paginatable
extend ActiveSupport::Concern
included do
before_action :set_pagination_params
end
private
def set_pagination_params
@page = params.fetch(:page, 1).to_i
@per = params.fetch(:per, 20).to_i.clamp(1, 100)
end
end
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
include Paginatable
def index
@articles = Article.active.page(@page).per(@per)
end
end
before_actionのようなクラスマクロ(クラス定義時に実行される設定用メソッド)も、includedブロックの中で問題なく使えます。
Concern乱用への注意
「なんとなく共通化」が招く問題
Concernは便利ですが、機能単位ではなく「なんとなく関連しそうなメソッド」を1つのモジュールに詰め込んでしまうケースが後を絶ちません。
こうしたConcernは名前から中身が推測できず、モデル本体を読んでも実際の挙動が追いにくくなります。
複数のConcernを1つのモデルにincludeしすぎると、そのモデルが実際にどのメソッドを持つのか把握するのに、Concernファイルを何個も往復する必要が出てきます。
Before/Afterで見る改善例
❌ Before:関連の薄いメソッドを1つのConcernに詰め込む
module UserHelperMethods
extend ActiveSupport::Concern
def full_name
"#{last_name} #{first_name}"
end
def send_welcome_email
UserMailer.welcome(self).deliver_later
end
def calculate_discount_rate
orders.count >= 10 ? 0.1 : 0.0
end
end
UserHelperMethodsという名前からは、氏名の整形・メール送信・割引計算という3つの異なる関心事が同居していることが読み取れません。
✅ After:関心事ごとにConcernを分割する
# app/models/concerns/nameable.rb
module Nameable
extend ActiveSupport::Concern
def full_name
"#{last_name} #{first_name}"
end
end
# app/models/concerns/discountable.rb
module Discountable
extend ActiveSupport::Concern
def calculate_discount_rate
orders.count >= 10 ? 0.1 : 0.0
end
end
send_welcome_emailのようなメール送信処理は、Concernではなく前回解説したService Objectに切り出す方が適しています。
Concernは「同じモデル内で完結する振る舞いの共通化」に向いていますが、外部サービスとの連携や複数モデルをまたぐ処理には向いていません。
実際にハマった循環参照エラー
私が実際につまずいたのが、Concern同士が互いを参照してしまったケースです。
Archivableの中でTaggableのメソッドを呼び出し、Taggableの中でもArchivableのメソッドを呼び出す実装をしてしまい、includeする順序によってNoMethodErrorが発生しました。
NoMethodError: undefined method `archived?' for #<Article:0x00007f9b1a1b2c10>
Concern同士に依存関係を持たせると、includeの順序に挙動が左右される壊れやすい設計になってしまいます。
Concernは「独立して成立する関心事」を切り出す仕組みだと割り切り、Concern間の直接依存は避けるのが安全です。
まとめ
この記事のポイント
ActiveSupport::Concernはincludedとclass_methodsで共通処理を簡潔に書ける仕組み- モデル・コントローラどちらでも、独立した関心事の共通化に使える
- 関連の薄いメソッドを1つのConcernにまとめると、可読性が下がる
- Concern同士を依存させると、includeの順序で壊れやすくなるため避ける
次に読むべき記事
→ 次の記事:Railsプロジェクトのディレクトリ構成のベストプラクティス
タグ: Ruby on Rails, 上級者向け, 設計