Ruby on Rails

Railsアンチパターン「Concern」とは何か?問題点の概要と背景を解説

目次

Railsアンチパターン「Concern」とは何か?問題点の概要と背景を解説

Ruby on Railsで提供されるActiveSupport::Concern(以下、Concern)は、複数のモデルやコントローラ間で共通する機能をモジュール化し再利用する仕組みです。Rails 4.0から導入されRailsガイド参照)app/models/concernsapp/controllers/concernsディレクトリに共通処理を切り出すことで、大規模なコードの理解・管理を楽にする狙いがありました。例えば、タグ付け機能など複数モデルに共通するロジックをConcernとして分離することで、コードの重複を防ぎDRY原則に沿った開発ができると期待されました。

一見するとConcernsは便利なリファクタリング手法に思えます。実際、DHH(Railsの作者)は自身のブログで「モデルの本質ではない部分を単一責任原則に則って別クラスに切り出すほどではないが、Concernを使えば手軽に分離できる」と述べています。つまり、Fat Model(肥大化したモデル)問題への対策として、Concernを用いてモデルの関心事をスライスし、オブジェクトの増えすぎを防ごうという発想です。しかし、こうしたConcerns活用の裏には落とし穴も存在します。経験上、安易にConcernに切り出すことでコードベースが複雑化し、かえってアンチパターンとなるケースが少なくありません。

本記事ではRailsのConcernをテーマに、Concernがなぜアンチパターンと呼ばれることがあるのか、その代表的な問題点と背景を解説します。また、Concernsと上手に向き合う方法や代替アプローチ、ベストプラクティスについても紹介します。「便利だから」と何でもConcernにしてしまう前に、ぜひ立ち止まって設計を見直すヒントにしてみてください。

RailsのConcernの基本:モジュールによる共通機能のミックスイン

まず、RailsにおけるConcernの基本を押さえておきましょう。ConcernはRubyのモジュールを用いたMix-inパターンの一種で、ActiveSupport::Concernをextendしたモジュールとして定義します。以下に簡単な使用例を示します。

# app/models/concerns/taggable.rb
module Taggable
extend ActiveSupport::Concern
included do
has_many :taggings, as: :taggable, dependent: :destroy
has_many :tags, through: :taggings
end
def tag_names
tags.map(&:name)
end
end
利用側のモデル
class Post < ApplicationRecord
include Taggable
end

上記の例では、Taggableモジュールを定義し、モデルPostでincludeすることで、タグ付けに関する関連付けやメソッドをPostモデルにミックスインしています。Concernを使うメリットは、複数のクラスで共有したいメソッドやスコープ、コールバック等を一箇所にまとめておける点です。同じコードを複数ファイルに書かずに済むためDRYになり、コードの重複によるバグも減らせます。また、Fat ModelやFat Controllerを回避し、コードを見通しよく整理できるように見えます。

しかし、Concernsは使い方を誤ると諸刃の剣です。上記の例のように「複数クラスで本当に再利用される共通関心事」をモジュール化する場合は有用ですが、実際のプロジェクトでは「とりあえずファイルを分けたい」「他でも使うかも」といった理由で乱用されるケースがあります。その結果、関心事が適切に分離されていないConcernが量産され、かえってコードの追跡が難しくなるなど問題が生じます。次節から、Concernが抱える具体的な問題点を見ていきましょう。

RailsのConcernsは本当に悪なのか?正しく使えば有用なのか、メリット・デメリットを考察

RailsのConcernに対しては、「便利な仕組みだ」という肯定的な意見と、「アンチパターンだ」という否定的な意見が混在しています。結論から言えば、Concernそのものが絶対的に悪というわけではありません。適材適所で使えばコードの再利用性を高める有用なツールになり得ます。しかし一方で、使いどころを誤れば技術的負債になりかねないリスクも孕んでいます。ここではConcernsのメリットとデメリットを整理し、正しい使い方次第で有用になり得る場面と、注意すべきポイントを考察します。

Concernsがもたらすメリット:重複排除とコード整理

まず、Concernsのメリットから確認しましょう。最大の利点は、共通処理の重複を排除できることです。同じロジックを複数のモデルやコントローラで使っている場合、それらをConcernとして一箇所にまとめれば、修正も一度で済み保守性が向上します。例えば、「複数のモデルで共通するバリデーション」や「共通のユーティリティメソッド」があればConcern化することでDRYにできます。また、コードの論理的なまとまりごとにファイルを分けることで、1ファイルあたりの行数が減り見通しが良くなる(ように感じられる)のも利点です。Rubocopなどの静的解析ツールでクラス長の警告が出た際に、Concernへ切り出すと警告を解消できるため、一種のリファクタリング手段として用いられることもあります。

さらに、共通機能をチームで共有しやすいという点もメリットです。例えばRailsアプリケーション内で標準化されたロジック(ログ出力やフォーマット処理など)をConcern化しておけば、新たなモデルで同様の機能が必要になった際にincludeするだけで利用できます。これにより、実装のばらつきを減らし、一貫性を保つことができます。

Concerns乱用のデメリット:コードの不透明化と依存関係の複雑化

一方で、Concernsのデメリットとして真っ先に挙がるのは、コードの所在が不透明になることです。メソッドの実体が別ファイルに切り出されるため、あるクラスの挙動を理解するにはそのクラスがincludeしている全てのConcernファイルを追いかける必要があります。特に、1つのクラスで複数のConcernをincludeしている場合、メソッド呼び出し元を探すのは手間です。例えば「このremoveメソッドはどこで定義されているのか?」を調べるために、候補となる複数のモジュールファイルを開かなければならず、開発者の認知負荷が高まります。

また、Concernsを多用すると依存関係の複雑化を招きます。Concern内部でinclude先(本体のクラス)のメソッドやデータに頼っているケースがあり、これにより暗黙的な双方向依存が生まれることがあります。例えばConcern内で「@modelのインスタンス変数がある前提」で処理を書くと、そのConcernは特定のクラスに密接に結びついてしまいます。そのクラス側もConcernに合わせた構造を維持しなければならず、結果として強い結合が生まれます。このような暗黙の依存は拡張や変更に弱く、モデル側のちょっとした変更(メソッド名の変更等)でConcernが機能しなくなる恐れがあります。

さらに、Concernsはバグの原因が見えにくくなるという問題もあります。Concern内部でコールバック(before_saveなど)や状態変更を行っていると、モデル側では一見何もしていないのに裏で値を書き換えている、という状況が起こり得ます。デバッグ時には「いつの間にかデータが変わっている」現象に直面し、その原因がどのConcernか特定するのに時間を要することになります。このように振る舞いの分散による不透明さが、Concerns乱用の大きなデメリットなのです。

適切に設計されたConcernは有用:条件と限界

以上を踏まえると、「Concernsは便利だが無秩序に使うと害も大きい」というのが実情です。では「正しく書かれたConcern」とはどのようなものでしょうか。それは一言で言えば、単一の明確な責務を持ち、依存関係が少ないConcernです。具体的には:

  • 限定的な責務: Concernの中身は極力一つの目的に絞り、関連するメソッド群のみ含める(複数の異なる機能を詰め込まない)。
  • 依存しない設計: Concern内のコードはinclude先のクラスに過度に依存しない。可能であれば引数で必要なデータを渡し、インスタンス変数などに頼らない。
  • 複数クラスで再利用: そのConcernが本当に二つ以上のクラスで使われる場合に限りモジュール化する(1クラスでしか使わないならモジュールにせず直接書いた方が明示的)。
  • Framework寄りの役割: ビジネスロジックではなく、ログ記録や通知送信などインフラ・フレームワーク共通処理に留める。

これら条件を満たすConcernであれば、コードベースの中で適度に責務を分割しつつ再利用性を高めるツールとして機能します。言い換えれば、Concernは「どんな問題も魔法のように解決する万能薬ではなく、使い所を見極めれば効果を発揮するスパイス」のような存在です。Concernsを完全に忌避する必要はありませんが、用いる際には今挙げた条件を満たしているかを自問することが重要でしょう。

RailsにおけるConcernの基本的な使い方と例:正しく書かれたConcernの実装ポイント

ここではRailsでのConcernの具体的な使い方と、望ましい実装ポイントを確認します。まずは基本的なConcernの定義方法から見てみましょう。Concernは通常、Railsプロジェクトのapp/models/concernsまたはapp/controllers/concernsディレクトリにモジュールとして作成します。モジュールはActiveSupport::Concernをextendして記述することで、クラスメソッドの定義やincludedブロック内での設定を簡潔に行えます。

Concern定義とinclude方法の基本

Concernを定義する際は、以下のような構造になります。

# app/models/concerns/my_concern.rb
module MyConcern
extend ActiveSupport::Concern
included do
# include先のクラスで実行したい設定(例: コールバックやスコープ)
end
# インスタンスメソッド
def instance_method_example
# ...
end
# クラスメソッド
module ClassMethods
def class_method_example
# ...
end
end
end

included do ... endブロックの中に、before_actionhas_manyなど、include先で評価させたいコードを記述できます。また、ClassMethodsモジュール内にメソッドを定義すると、それがinclude先のクラスメソッドとして追加されます。ActiveSupport::Concernを使うことで、Ruby標準のModule#includedを自前で書くよりも簡潔に記述できる利点があります。

定義したConcernを利用するには、対象のクラスでinclude MyConcernと記述するだけです。Railsではconcernsディレクトリ以下のモジュールは自動的にロードされるため、requireは不要です。注意: 名前空間に気を配り、クラス名と被らないようモジュール名を付けましょう(例ではMyConcern)。

Concernsの適用例:複数クラスでの共通機能をモジュール化

実際にどのような場面でConcernが活躍するか、簡単な例で考えてみます。例えば「ユーザーに紐づく最新の投稿3件を取得する」処理が複数のコントローラで必要になったとします。この共通処理をConcernにまとめる場合、以下のようになります。

# app/controllers/concerns/recent_postable.rb
module RecentPostable
extend ActiveSupport::Concern
def fetch_recent_posts(user)
user.posts.order(created_at: :desc).limit(3)
end
end
利用する各コントローラで
class UsersController < ApplicationController
include RecentPostable
def index
@recent_posts = fetch_recent_posts(current_user)
end
end

上記では、コントローラ間で重複しがちな「ユーザーの最新投稿取得ロジック」をConcern RecentPostableに抽出しました。複数のコントローラ(例えばUsersControllerとAdminsControllerなど)でinclude RecentPostableすれば、同じfetch_recent_postsメソッドを共有できます。このように本当に複数箇所で使われる処理をConcern化するのは、合理的な利用例と言えます。

重要なのは、このConcernがビジネスロジックの所在を不明確にしないかを検討することです。上記例では「最新投稿を取得」という処理がUserモデル寄りかController寄りか議論になるところですが、Userモデル側にもlatest_postsのようなメソッドを用意し、そこから呼び出す方が直感的かもしれません。Concernに切り出す前に、「この処理はどの層に属するロジックか?」を考える習慣が大切です。複数箇所で使うからと安直にConcern化するのではなく、モデルに置くべきものか、あるいは専用のクラスにすべきかをまず検討しましょう。

良いConcernの実装ポイント:依存性の排除と単機能

では、「正しく書かれたConcern」を実装する際のポイントを改めて整理します。繰り返しになりますが、良いConcernは単一の責任に徹し、かつ依存性が低いことが重要です。実装上は以下の点に注意します:

  • 依存データは引数で受け取る: Concern内でinclude先のインスタンス変数やメソッドに直接アクセスしない。必要な値はメソッドの引数として渡す設計にする(依存の注入)。例えばConcern内でuseritemsといったメソッドに頼るのではなく、calculate_tax(user, items)のように引数で受け取る。
  • 副作用の明示: Concernが裏でコールバックを登録したりデータを変更したりする場合、処理の意図が分かるようにメソッド名やコメントで明示する。例えばbefore_saveで日付を調整するConcernなら、メソッド名をadjust_published_date!のように副作用が推測できる名前にする。
  • 1 Concern = 1 機能: 一つのConcernに複数の無関係な機能を詰め込まない。共通だからといって何でもかんでも詰めると、結局そのConcern自体が巨大化してしまうためです。
  • 単体テスト可能に: Concern単体でもテストしやすい設計にします。具体的には、モジュールをincludeしたダミークラスを用意してメソッドを検証するなど。依存が少なければテストも容易です。

以上の点を守れば、Concernを使用していてもコードの見通しや保守性を大きく損ねずに済むでしょう。それでもなお、「Concernを使わず別のアプローチで実現できないか?」は常に念頭に置くべきです。次章から、Concernのアンチパターンと、代替策について具体的に見ていきます。

安易にConcernsとして切り出す前に考えるべき設計上の注意点とその影響

便利なConcernsですが、安易にコードを切り出す前に検討すべき設計上の注意点があります。ここでは、「とりあえずConcernに入れておけばOK」と考えてしまいがちなケースに待ち受ける弊害を解説します。特にRails開発でありがちなFat Model/Fat Controller問題に対して、Concernで対応しようとする際の注意点を見ていきます。

Fat Model/Controller対策にConcernを乱用する危険

Railsのプロジェクトでは、モデルやコントローラが肥大化するFat Model・Fat Controller問題がしばしば発生します。そこで「クラスが大きいなら、一部をConcernに切り出してファイルを分割しよう」と考えるのは自然な発想です。しかし、これを安易に実行するのは危険です。Concernに切り出してファイル上はスリムになっても、実行時には依然としてそのモデル/コントローラが全ての責務を担っているからです。言わば、見た目だけのリファクタリングになってしまいます。

例えば、1000行のUserモデルを心配して、バリデーション関連、計算ロジック関連、通知関連などをそれぞれConcernに分割したとします。ファイルは複数に別れましたが、Userモデルは相変わらずそれら全ての機能をもつ巨大な存在であることに変わりありません。結果としてクラスの責務自体は減っておらず、開発者が把握すべき知識(Userモデルに何ができるか)は減っていないのです。この状態は、部屋の散らかった荷物を別の箱に移して隠しただけで、部屋自体の片付けは終わっていないのと似ています。

RubocopのClassLength警告に対処するための安易なConcern分割

静的コード解析ツールRubocopでは、クラスの行数が一定以上だとClassLengthという警告を出します。この警告を黙らせる目的で、クラスをConcernsに分割するケースも見られます。例えばPostモデルが規定の300行を超えたので、PreviewableReservableといったConcernsに機能を小分けにしてincludeする、といった対応です。

確かにファイル上はPostモデルがスリムになりCIも通るでしょう。しかし、これも前述の通り本質的な解決になっていません。そのConcernが他では使われずPostモデル専用なのであれば、実質的に単にファイルを分割しただけです。Rubocopの警告は「そのクラスが責務過多である」サインのはずですが、Concern分割は責務を減らしていないため問題の先送りでしかありません。むしろコードを追うファイルが増えて可読性を下げる結果にもなりかねません。

Railsには、Fat Modelのコードを視覚的に整理する方法としてModule::Concerningという仕組みもあります。例えば、Postモデル内でconcerning :Reservation do ... endのように書けば、ファイル内で論理的にコードをセクション分けできます。これはあくまで見通しをよくするテクニックで、責務の分散ではありませんが、「名前を付けて切り出す」メリットを同一ファイル内で享受できる方法です。Rubocop対応で焦ってConcernに分けるくらいなら、concerningで整理する方が、まだコンテキストが一箇所にある分読みやすいでしょう。

コードの複雑さは分割では減らない:凝集度と責務の見直し

大事なポイントは、「コードの複雑さは物理的なファイル分割では減らない」ということです。本当に必要なのは、クラス自体の凝集度を見直し、責務を適切に分割することです。Fat Modelであれば、そのモデルが担っている複数の責務を洗い出し、別のクラス(例えばサービスオブジェクトやフォームオブジェクト、値オブジェクトなど)に切り離す方が意味があります。つまり、論理的凝集から機能的凝集へと再編成することが根本解決策です。

Concernsは簡単に分割できるため魅力的ですが、前述のように安易な適用は禁物です。まずは「このクラスが肥大化した原因は何か?」「どの責務を切り出すべきか?」を分析しましょう。もし複数の責務が混在しているなら、それぞれ別のクラス/モジュールに切り出すべきですし、一枚岩のドメインロジックであれば無理に分割せずそのままにしておく方が保守性が高い場合もあります。重要なのは、Concernに逃げ込む前に設計を見直すことです。

Concern以外の代替策を検討:サービスクラスやデザインパターンの活用

Fat Model/Controllerの解消策として、Concern以外にも様々なアプローチがあります。例えばサービスオブジェクト(Plain Old Ruby Objectに処理を委譲)を使って、ある特定の機能をクラスごと独立させる手法は有名です。また、フォームオブジェクトを導入して、モデルのバリデーションロジックを別オブジェクトに持たせることもできます。これらの方法では、Concernとは違いオブジェクト間の関係が明示的になります。オブジェクトの責務がはっきり分かれ、テストもしやすくなるという利点があります。

他にも、デザインパターンであるDecoratorを使って機能追加する方法や、Railsの機能であるConcerns for routing(ルーティングの関心事分離)等、問題に応じて適切な解決策が存在します。大切なのは、「本当にConcernを使う必要があるのか?他の方法で目的を達成できないか?」と自問する姿勢です。Concernsは手軽な反面、乱用すると構造を見えにくくします。設計段階で一歩立ち止まり、代替策を検討することで、より健全なコードベースを保つことができるでしょう。

ビジネスロジックをConcernで共通化することの問題点とアンチパターンの具体例

Rails Concernsのアンチパターンとして典型的なのが、ビジネスロジックの共通化を目的にConcernを使ってしまうケースです。本来、ビジネスロジックはモデルやサービスオブジェクトに集約すべきですが、「複数のコントローラで似た処理をしているからDRYにしよう」と、Concernに抜き出してしまう例がしばしば見られます。この章では、その具体例と問題点、正しい対処法を示します。

コントローラ間の重複処理をConcernで共通化するケース

例えば、あるRailsアプリで複数のコントローラに同じビジネスロジックが書かれている状況を考えます。先ほどの例にもあったように、「同カテゴリーのPostを取得する処理」がPostsControllerやCategoriesControllerなど複数箇所に重複しているとしましょう。開発者はこれをDRYにするため、共通部分をConcernにまとめて各コントローラでincludeするという手段を取りがちです。こうすることで、一見コードの重複はなくなり、修正も一箇所で済むように思えます。

実際、共通処理の関数化・モジュール化自体は悪いことではありません。問題は「その処理はどこに属するロジックか?」という点です。コントローラ同士で重複しているということは、本来はモデルに持たせるべき処理である可能性が高いのです。Concernに逃げる前に、まずモデル側にそのロジックを移動できないか検討するべきです。

モデルに書くべきロジックをConcernにしてしまう弊害

ビジネスロジックをモデルではなくConcernに書いてしまうと、ロジックの所在が不明瞭になります。例えば、Userに関連するデータ取得処理をUserモデルではなくControllerのConcernに書いた場合、将来的にその処理を修正・拡張したい時に「どこを直せばいいのか」直感的に分かりにくくなります。開発者がモデルを開いても見当たらず、Concernファイルまで探さなければなりません。これは可読性・保守性の低下につながります。

また、モデルが本来負うべき責務を他所(Concern)に書くことは、設計思想的にもアンチパターンです。Railsにおいて、ビジネスロジックは極力モデル(ActiveRecordモデル)に寄せることが原則とされています。モデルが肥大化するのを恐れるあまりConcernに逃がすのは本末転倒で、肝心のドメイン知識が分散してしまいます。

アンチパターンの具体例:コントローラのConcern化による問題

実際のコード例で考えてみましょう。次のようなPostsControllerがあったとします。

class PostsController < ApplicationController
def show
@post = Post.find(params[:id])
@same_category_posts = same_category_posts(@post)
end
private
def same_category_posts(post)
category_ids = post.category_ids
same_post_ids = PostCategory.where(category_id: category_ids).pluck(:post_id)
Post.where(id: same_post_ids - [post.id]).includes(:categories).limit(5)
end
end

上記では、same_category_postsというプライベートメソッドで投稿と同じカテゴリの他投稿を5件取得しています。仮にこれと同じメソッドが別のコントローラにも存在した場合、DRYのために以下のようにConcernに切り出すことが考えられます。

# app/controllers/concerns/post_findable.rb
module PostFindable
extend ActiveSupport::Concern
def same_category_posts(post)
category_ids = post.category_ids
same_post_ids = PostCategory.where(category_id: category_ids).pluck(:post_id)
Post.where(id: same_post_ids - [post.id]).includes(:categories).limit(5)
end
end
PostsControllerでの利用
class PostsController < ApplicationController
include PostFindable
def show
@post = Post.find(params[:id])
@same_category_posts = same_category_posts(@post)
end
...
end

一見、メソッドがControllerから消えてスッキリしました。しかし、これは良くないリファクタリングです。そもそもsame_category_postsはビジネスロジックであり、コントローラではなくモデルに書くべきだからです。この例で言えば、Postモデルに同様のメソッドを移すのが筋でしょう。

なぜアンチパターンか:ロジック分散による保守性低下

上記のようなConcernリファクタリングがアンチパターンである理由は、主に保守性の低下にあります。ビジネスロジックがController Concernとモデルに分散して存在すると、ある機能について変更やバグ追跡をする際に参照すべき箇所が増えてしまいます。例えば「同カテゴリの投稿取得ロジックにバグがある」と判明したとき、それがモデル側なのかConcern側なのか、複数箇所をチェックしなければなりません。

また、ControllerのConcernはControllerの文脈で動くとはいえ、モデルと離れた場所にビジネスロジックが書かれている状況に変わりありません。モデルと違い、Concernはどのクラスからでもinclude可能なため、将来的に別の場所から意図せず使われてしまうリスクもあります。そうなると「どこで使われているかわからない共有関数」と化し、修正のインパクト調査が難しくなります。

このように、ビジネスロジックをConcernに逃がすことは短期的な重複解消にはなるが、長期的なコードの健全性を損ねる可能性が高いのです。

正しい解決策:共通ビジネスロジックはモデルやサービス層へ集約

ビジネスロジックの重複を解消したい場合、まずはモデルに集約することを検討しましょう。先の例では、Postモデルにsame_category_postsメソッドを追加すれば、Controller側では単に@post.same_category_postsと呼ぶだけで済みます。モデルにロジックを寄せれば、関連する処理が一箇所にまとまり、見通しも良くなります。モデルが大きくなることを懸念するかもしれませんが、それはそのモデルが本来持つべき責務なのですから問題ありません(むしろそれを避けるためにConcernに逃がす方が問題です)。

それでもモデルが肥大化するなら、サービスオブジェクトなど別の層を導入しましょう。例えばPostsFetcherサービスを作り、そこにsame_category_posts機能を持たせるのも手です。このクラスを呼び出すようにすれば、ロジックの置き場所が明確になります。

重要なのは、「重複を解消する=すぐConcern」と短絡しないことです。Railsの設計原則(Fat Model推奨など)に立ち返り、適切な層にロジックを集める方が、結果的に健全なコードになります。Concernは最後の手段くらいに捉えて、まずはモデルorサービス層での対応を検討しましょう。

Concern乱用による可読性低下・技術負債の蓄積:その原因と影響を検証

Concernsを乱用したプロジェクトでは、徐々にコードの可読性が低下し、技術負債が蓄積する傾向があります。この章では、Concern過多がもたらす具体的な弊害を掘り下げ、その原因を分析します。

関心事が散逸することでコード理解が困難に

Concernsを多用すると、ある機能に関するコードが複数のモジュールに分散してしまう恐れがあります。例えば一つのモデルに関連する振る舞いが、モデル本体と3つのConcernに分かれて定義されているような場合です。開発者はそのモデルの全貌を理解するために4ファイルを行き来しなければなりません。関心事(Concern)の散逸により、コードベースの把握コストが大きく上がります。

特に、Concernsが入れ子(ネスト)になっていると事態は深刻です。あるConcernがさらに別のConcernをincludeする「マトリョーシカConcern」状態では、実際にどのモジュール由来のメソッドが動いているのか追跡するのが困難になります。UserクラスがConcern Aをincludeし、AがBをinclude、BがCをinclude…という状況では、UserにとってCのメソッドも使えるものの、定義箇所を辿るのは迷宮入りしかねません。定義元の追跡不能は可読性を著しく低下させます。

暗黙的な振る舞いの増加でデバッグ難易度が上昇

Concerns乱用プロジェクトでは、暗黙の振る舞いが増えることもしばしばです。前述したようにConcern内でコールバックを定義しているケースはその典型例で、コード上には呼び出しも何も書いていないのに、includeしただけで何かが起こるという状況です。このような隠れた動作は、エラーやバグ発生時の原因特定を難しくします。

例えば「レコード保存時に勝手にデータが書き換わる」というバグがあった場合、開発者はモデルやコントローラのコードを確認しても原因が分からず頭を抱えるでしょう。実はConcern内のbefore_saveで起こっていた…というオチを突き止めるには、まずそのConcernの存在に気付かねばなりません。Concernsが増えるほど、「何かおかしい…さて、どのConcernが関与しているのか?」と調査する手間が増大します。暗黙性の高さ=バグ調査コストの増大という図式は見逃せません。

モジュール間の依存関係が複雑化するリスク

Concernsの誤用によって起こる問題に、双方向あるいは三角関係の依存もあります。Concern AがクラスBの詳細に依存し、Bがまた他のモジュールに依存…といったケースです。モジュール内でdefined?(some_method)を使ってinclude先にメソッドがあることを前提にしていたり、Concern同士がお互いの存在を前提にして呼び出し合ったりすると、非常に脆い構造になります。

依存関係が複雑になると、変更の影響範囲を予測しづらくなります。一箇所の修正が思わぬ部分に波及してバグを生む危険が増すため、開発速度も低下します。Concernsは軽量なMix-inゆえにこの罠に陥りがちで、密結合なコードを作ってしまうリスクが常につきまといます。新たな開発者がコードを引き継いだ際にも、理解に時間がかかる要因となるでしょう。

テストやリファクタリングの難易度上昇

Concernを多用する設計は、テストの面でも不利に働く場合があります。モジュールをincludeした状態で振る舞いをテストする必要があるため、テストのセットアップが煩雑になったり、モジュールごとの単独テストが難しかったりします。特に複数のConcernが絡み合うと、その組み合わせごとの検証が必要になり、網羅的なテストを書くのが大変です。

また、一度Concernベースで作ってしまうと、大規模なリファクタリングにも尻込みしやすくなります。Concernを廃止して別の設計に改めようとすると、多数のクラスにまたがる変更になるためリスクが高く、結果として「負債」を抱えたまま進むことになりがちです。技術的負債とはまさにこうした状況を指します。Concerns乱用は一時的な開発効率を上げるかもしれませんが、長期的にはリファクタリングコストを跳ね上げ、コードの柔軟性を奪う可能性が高いのです。

長期的な技術負債の蓄積とプロジェクトへの影響

以上のような問題が積み重なると、プロジェクト全体として技術的負債の蓄積という深刻な状態に至ります。具体的には、コードが読みづらくバグの温床になっているために新機能追加や変更のたびに余計な労力がかかる、下手に手を入れられない箇所が増える、といった状況です。Concerns乱用が招く可読性低下・複雑化は、この技術負債をジワジワと増やしていきます。

例えば「ここに新しい機能を追加したいが、関連するConcernsが多すぎて副作用が怖い」「古いConcernが何をしているか分からず放置されている」といった事態になれば、開発スピードも品質も低下します。最悪、コードの構造が複雑すぎてリファクタリングもできず、迂闊な変更ができない泥沼に陥るかもしれません。

こうならないためにも、早い段階でConcern依存から脱却し、コードの構造を見直すことが重要です。次の章では、Concernsを使う場合の注意点やベストプラクティスを述べ、負債を生まない上手な付き合い方を提案します。

RailsでConcernsを使う際の注意点とベストプラクティス:効果的な活用方法を解説

これまでConcernsの問題点を中心に述べてきましたが、最後にConcernsと上手に付き合うための注意点やベストプラクティスをまとめます。適切な指針に沿って利用すれば、Concernsも有益な道具となり得ます。

Concerns使用の前提:責務が限定された場合のみ採用する

Concernsを使うか迷ったときは、「そのモジュールに切り出す機能は責務が明確で限定的か?」と自問しましょう。例えばログ出力や通知送信のように、アプリケーションの本質的なドメインから一歩引いた共通処理であればConcern化の候補になりえます。一方、ドメインロジックそのものは原則としてConcernにすべきではありません。Concernを採用する前提条件として、扱う関心事がアプリケーション全体で共有される副次的なものに限る、という意識を持ちましょう。

また、そのConcernが本当に複数のクラスで使われるかも重要な判断基準です。1クラスでしか使われないなら、最初からそのクラス内部に留めておくべきです。安易に「いつか他でも使うかも」でモジュール化せず、実際に必要になってから抽出する方が無駄がありません。

1モジュール1機能:単一責任の原則を守る

Concernを作る際は、単一責任の原則を徹底します。1つのConcernが持つ機能は基本1種類、それに関連するメソッドやコールバックだけに絞ります。欲張って複数の機能を詰め込むと、そのConcern自体が肥大化して管理しにくくなります。例えば「認証系」と「ログ出力系」で切り分けるなど、役割ごとにモジュールを分けましょう。

また、ファイル名・モジュール名から何をするConcernか分かるように命名するのも大切です。名前が長くなっても構いませんので、Auditable(監査ログ記録用)やTaggable(タグ付与用)のように、機能を的確に表す名前を付けてください。名前を見ただけで役割が伝わるConcernは、メンテナンス性が高まります。

依存性の排除:include先に依存しない設計

前述のように、良いConcernの条件は依存性が低いことです。実装上は、Concern内でinclude先(本体クラス)のメソッドやデータにできるだけ依存しない書き方を心がけます。例えばConcern内でself(=include先のオブジェクト)をあまり参照せず、必要な情報は引数経由でもらうか、処理の中で完結させます。

どうしてもinclude先のメソッドに依存する場合は、その旨をコメントやコードで明示しましょう。先の例にもあったように、raise NotImplementedErrorで前提となるメソッドが存在しなければ例外を出す、などの工夫です。これにより、モジュールをincludeする側への注意喚起となり、不用意な誤用を防げます。

複数のConcernに跨る処理はできるだけ避ける

一つの機能を実現するのに複数のConcernをまたいで実装しないようにしましょう。例えばある処理の前半をConcern A、後半をConcern Bに書くようなことは避けます。処理の流れが断片化するとコードを追いづらくなるだけでなく、Concern間の順序依存も発生しかねません(特にコールバック順序など)。基本的に、1つのConcernで完結するよう実装し、それ以上に複雑になる場合はサービスクラス化を検討した方が良いでしょう。

もし複数Concernをどうしても使う場合でも、それぞれが独立して動くように設計します。Concern同士が互いにsuper呼び出しを要求するような初期化処理(前述のinitializeの例)などは非常に危険です。Concernは基本的に他のConcernに依存せず個別に機能するユニットであるべきです。

代替パターンの活用:委譲やCompositionで明示的な設計に

最後に、Concern以外の手段も積極的に活用しましょう。前述したサービスオブジェクトやフォームオブジェクトへの委譲はもちろん、簡単な共通処理ならスタティックなユーティリティクラスや継承で解決できる場合もあります。Railsではない純粋なRubyクラスを作ってそこに処理を書き、それを呼び出す形にすると、Concernsよりも明示的で追いやすいコードになることが多いです。

また、Rails 7以降ではモジュールを用いた新たなアプローチ(例えばHotwireやConcerns for routingなど)も登場しています。常に最新のベストプラクティスをウォッチし、プロジェクトに適した方法を選択しましょう。重要なのは、「明示的で読みやすいコードが最善」という原則です。Concernsに頼りすぎず、適切なデザインパターンを組み合わせることで、可読性と再利用性を両立させることができます。

「Fat Model」をConcernsで解決しようとする危険性とその代替案を詳しく解説

最後に、Rails開発でよく問題になる「Fat Model」をConcernsで解決しようとすることの危険性についてまとめます。Fat Modelとは、一つのActiveRecordモデルが肥大化しすぎて様々な責務を抱え込んでいる状態を指します。これを解消しようとして、単純にコードをConcernsに分割するアプローチは先述の通り危険です。ここでは改めてその理由と、より良い代替策を解説します。

Fat Model問題とは:モデル肥大化の背景

Fat Model問題は、Railsに限らずドメインモデルが巨大化する現象です。機能追加や変更を繰り返すうちに、モデルクラスにビジネスロジックや関連コードが山盛りになり、何でもかんでも詰め込まれたGod Object化してしまうことがあります。Railsの設計理念では「Fat Model, Skinny Controller(モデルは太く、コントローラは細く)」が推奨されるため、どうしてもモデルにロジックが集中しがちで、この問題が生じやすい土壌があります。

Fat Modelになる背景には、開発初期にはシンプルだったモデルに、後付けで機能が継ぎ足されていくケースが多いです。また、適切なオブジェクト分割を怠ったままスピード優先で実装していくと、いつの間にかクラスが肥大化しているということもあります。Fat Modelそのものはアンチパターンと言われつつ、ある程度は許容される側面もあり、判断が難しい点でもあります。

誤った解決策:Concernによるファイル分割

Fat Modelへの対処法として真っ先に浮かぶのが「Concernで分割して見た目を細くする」ことです。一時的にはモデルファイルの行数が減り、Fat Modelが解消したかのように感じます。しかし繰り返しになりますが、これは誤った解決策です。Concernで分割しただけでは、モデルが抱える責務の数(複雑さ)は少しも減っていません。実行時にはモデルに全Concernの機能が混入するので、相変わらずFatなオブジェクトなのです。

むしろ、ファイルが分かれた分だけコードの所在が不透明になり、以前より状況が悪化する恐れすらあります。特にFat Model解消のために複数のConcernにまたがって機能を実装すると、モデル本体にはinclude行だけが並び、実処理は散逸しているという状態になります。これは非常に読みづらく、モデルの振る舞いを理解するのに全てのConcernファイルを開かなければなりません。ファイル分割リファクタリングの罠とも言えるでしょう。

ファイルを分けるだけでは責務は減らない

繰り返しになりますが、単にファイルを分割するだけでは責務は減りません。Fat Model問題の本質は、1クラスに責任が集中しすぎていることです。従って解決すべきは、責務の適切な分散・移譲であって、ファイル数の問題ではありません。Fat ModelをConcernで分割する行為は、問題の本質に手を付けずカモフラージュしているだけなのです。

たとえばUserモデルが肥大化しているなら、その中に潜む異なる役割(認証ロジック、通知ロジック、集計ロジック等)を見極め、それぞれ別のクラスやモジュールに機能的凝集として切り出すことを検討すべきです。先述のサービスオブジェクトや、特定の機能に特化したサブクラス、あるいはRubyのSimpleDelegator等を使った委譲でも良いでしょう。重要なのは、問題となっている責務に直接メスを入れることです。

Fat Modelへの真の対処法:ドメインロジックの整理とクラス設計の見直し

Fat Modelを健全な状態にするには、ドメインロジック全体を整理し直し、クラス設計を改める必要があります。具体的には以下のような手順が有効です:

  1. 責務の洗い出し: 該当モデルが担っている機能・役割を列挙します。カテゴリごとにグルーピングし、「これは別クラスにすべき」「この機能はモジュール共通化できる」等を検討します。
  2. クラスの再分割: 洗い出した結果に基づき、新たにクラスやモジュールを作成します。サービスオブジェクトやFormオブジェクト、Policyオブジェクトなど、適切なパターンを採用して分割します。
  3. 移行とテスト: 機能を新しいクラスへ移し、既存コードから呼び出すよう修正します。併せてテストを用意し、挙動が変わらないことを確認します。
  4. 不要なConcernの撤廃: もしFat Model解消のために導入していたConcernがあれば、役目を終えた段階で削除します。コードベースから不要な抽象化を取り除き、シンプルな構成に戻します。

このように大規模リファクタリングになる場合もありますが、一度やってしまえば以降の開発が格段にスムーズになります。Fat Modelは放置すると開発効率をじわじわ低下させますから、思い切って手を入れることも検討しましょう。

代替案の例:サービスオブジェクトやFormオブジェクトで責務を分散

Fat Modelを解消する具体的な代替アプローチとして、すでに何度か触れたサービスオブジェクトやFormオブジェクトがあります。サービスオブジェクト(たとえばUserNotifierOrderProcessorなど)は、1つのユースケースや処理フローをクラスとして切り出す方法です。Fat Modelから一連の処理を抜き出してサービスクラスに委譲すれば、モデル本体はだいぶスリムになります。Formオブジェクトは、モデルが持っていた複雑なバリデーションロジックを独立したオブジェクトに任せる手法で、ユーザー入力処理の整理に有効です。

他にも、Decoratorパターンでモデルにメソッドを動的付与する方法や、Composableオブジェクト(小さなモジュールやクラスを組み合わせる)を使う手もあります。Railsの世界ではありませんが、例えばPORO (Plain Old Ruby Objects)を用いたオブジェクト指向設計を取り入れると、ActiveRecordモデルの肥大化を防ぎやすくなります。Concernsに頼らずとも、Rubyのオブジェクト指向の力で十分対処可能なのです。

これら代替案に共通するのは、明示的な責務分割が行われる点です。Concernのように見えなくなるところに隠すのではなく、クラス構造としてはっきり分けることで、コードの意図が明確になります。結果として、新たに参加したエンジニアでも追いやすいコードとなり、将来的な変更にも強くなるというメリットがあります。

まとめ: RailsのConcernsは、本来便利な再利用パターンですが、安易な利用はアンチパターンになり得ます。「とりあえずConcernで切り出す」は禁物です。Concernsと向き合う際は、まず設計を見直し、代替手段も含めて検討することが重要です。ファイルを分けて一時的にスッキリさせるのではなく、責務そのものを整理するアプローチを優先しましょう。どうしてもConcernを使う場合は、単一責任・低依存・再利用性といった原則を守り、本当に可読性が上がるのかを一度立ち止まって考えてみてください。適切に使えばConcernsも有用なツールです。大事なのは開発者がConcernsに振り回されず、コードの整合性と明瞭さを常に意識して保つことと言えるでしょう。

資料請求

RELATED POSTS 関連記事