RailsのConcerns(関心事):使うべきか、使わざるべきか
Ruby on Railsを使ったことがあるなら、concernsという概念に出会ったことがあるはずです。新しいRailsプロジェクトを作成すると、必ずapp/controllers/concernsとapp/models/concernsというディレクトリが用意されています。では、concernsとは一体何なのでしょうか?そして、なぜRailsコミュニティの人々の中には、concernsについて否定的な意見を述べる人がいるのでしょうか?
クイック概要
RailsのConcernとは、ActiveSupport::Concernモジュールをextendしたモジュールのことです。「モジュールと何が違うの?」と思われるかもしれません。主な違いは、Railsのconcernsでは次のような「魔法」が使える点にあります。
# app/models/concerns/trashable.rb
module Trashable
extend ActiveSupport::Concern
included do
scope :existing, -> { where(trashed: false) }
scope :trashed, -> { where(trashed: true) }
end
def trash
update_attribute :trashed, true
end
endincludedというキーワードに注目してください。これはRubyモジュールに振りかけられた、いわばRails風の「糖衣構文」です。ActiveSupport::Concernを使うと、includedブロック内に記述したコードを、includeされた側のコンテキストで評価できるようになります。たとえば、モデルからゴミ箱(trash)への移動ロジックを切り出したい場合、上記のようにincludedブロックを活用し、その後モデル側で以下のようにconcernを読み込めます。
class Song < ApplicationRecord
include Trashable
has_many :authors
# ...
endこの時点では非常に便利でシンプルに見えますよね。モデルは少しスリムになり、ゴミ箱ロジックはSongモデルだけでなく他のモデルでも再利用できるようになりました。しかし、物事は複雑になり得ます。詳しく見ていきましょう。
ミックスインの古典的な例
concernsの深淵へさらに踏み込む前に、もう少し説明を加えておきます。include SomeModuleやextend AnotherModuleのようなコードを見かけたら、それはミックスイン(mixin)と呼ばれるものです。ミックスインとは、他のクラスに追加できるコードの集合体です。Rubyのドキュメントにもあるように、モジュールはメソッドと定数のコレクションです。つまり、ここで行っているのは、メソッドや定数を持つモジュールをさまざまなクラスに取り込み、それらを利用可能にすることなのです。
先ほどのTrashable concernでやったのはまさにこれです。モデルオブジェクトをゴミ箱に入れる共通ロジックをモジュールとして抽出し、後で別の場所からincludeできるようにしました。このミックスインは、RubyやRailsだけでなく広く使われているデザインパターンです。しかし、どこで使われても、人々の評価は真っ二つに分かれます。「良い仕組みだ」と愛する者と、「簡単に制御不能に陥る」と嫌う者です。
この違いをより深く理解するために、concernsを使用するメリットとデメリットをいくつか見ていきましょう。そうすることで、いつ、あるいはそもそもconcernsを使うべきなのかが見えてくるはずです。
「すべてを手に入れる」問題
Trashableのようなconcernに処理を抽出すると、includeした場所のあらゆる機能にアクセスできるようになります。これは強力な力ですが、Richard Schneemanがこのテーマに関するブログ記事で述べたように——「大きな力には、コードを複雑にする大きな能力が伴う」のです。彼が言いたかったのは、本来concerns内にあるべきではないものまで依存してしまうコードが複雑化するということです。
もう一度Trashableを見てみましょう。
module Trashable
extend ActiveSupport::Concern
included do
scope :existing, -> { where(trashed: false) }
scope :trashed, -> { where(trashed: true) }
end
def trash
update_attribute :trashed, true
end
endこのconcernのロジックは、includeされる側のモデルにtrashedフィールドが存在することを前提としています。まあ、大したことありません。それが目的だったわけですから。しかし、実際によく起こるのは、モデルの他の要素までconcernに引き込みたくなる誘惑です。どのようにしてそれが起こるのかを説明するために、Songモデルにfeatured_authorsという別のメソッドがあると想像してみましょう。
class Song < ApplicationRecord
include Trashable
has_many :authors
def featured_authors
authors.where(featured: true)
end
# ...
end
class Album < ApplicationRecord
include Trashable
has_many :authors
def featured_authors
authors.where(featured: true)
end
# ...
end分かりやすくするために、同じくTrashableをincludeするAlbumモデルも追加しました。ここで、曲やアルバムがゴミ箱に入れられたときに、注目の著者たちに通知を送りたいとしましょう。多くの人は、このロジックをconcern内に次のように書きたくなります。
module Trashable
extend ActiveSupport::Concern
included do
scope :existing, -> { where(trashed: false) }
scope :trashed, -> { where(trashed: true) }
end
def trash
update_attribute :trashed, true
notify(featured_authors)
end
def notify(authors)
# ...
end
endここで、状況が少しずつ複雑になり始めます。ゴミ箱ロジックがSongモデルの外にあるため、通知処理もTrashable concernに入れたくなるのです。そしてそこで、何か「おかしな」ことが起こります。featured_authorsはSongモデルから引っ張ってこられています。さて、これがプルリクエストレビューとCIチェックを通過したとしましょう。
数ヶ月後、新しい要件が設定されます。開発者は、曲におけるfeatured_authorsの表示方法を変更する必要が出てきました。たとえば、ヨーロッパ出身の注目著者だけを表示したいという要件です。当然、開発者は注目著者が定義されている場所を探し、編集します。
class Song < ApplicationRecord
include Trashable
has_many :authors
def featured_authors
authors.where(featured: true).where(region: 'Europe')
end
# ...
end
class Album < ApplicationRecord
include Trashable
has_many :authors
# ...
endこれは著者を表示している箇所ではうまく動作しますが、本番環境にデプロイした後、世界中の他の地域の人々は自分の曲に関する通知を受け取れなくなってしまいます。このようなミスは、concernsを使っていると簡単に起こりえます。上記の例はシンプルで人工的なものですが、実戦のコードベースではもっと厄介なケースがあり得ます。
ここでのリスクは、concern(ミックスイン)がinclude先のモデルについてあまりにも多くを知ってしまうことにあります。これは循環依存(circular dependency)と呼ばれるものです。SongとAlbumはゴミ箱機能のためにTrashableに依存し、Trashableはfeatured_authorsの定義のために両方のモデルに依存しています。同様に、Trashable concernが動作するには両方のモデルにtrashedフィールドが必要であることも、同じ種類の依存関係だと言えます。
これこそが、反concern派が反対し、擁護派が賛成する理由です。私なら、コードベースでは最初のバージョンのTrashableを採用します。では、通知機能を含む2番目のバージョンをどう改善できるか見てみましょう。
メソッドの出所はどこに?
通知機能を含むTrashableを振り返ると、何とか手を打つ必要があります。concernsを使う際にもう一つ起こりがちなのが、DRY原則の過剰適用です。デモンストレーションとして、既存のモデルに対してもう一つのconcernを作成してみましょう(しばらくお付き合いください)。
module Authorable
has_many :authors
def featured_authors
authors.where(featured: true)
end
endすると、SongとAlbumは次のようになります。
class Song < ApplicationRecord
include Trashable
include Authorable
# ...
end
class Album < ApplicationRecord
include Trashable
include Authorable
# ...
endすべてをDRYにできましたが、今度は「ヨーロッパ出身の注目著者」という要件が満たせなくなりました。さらに悪いことに、Trashable concernと各モデルがAuthorableに依存するようになってしまいました。え、何それ?——以前concernsと格闘していたとき、私がまさに抱いた疑問です。メソッドがどこから来ているのかを追跡するのは非常に困難になります。
私の解決策は、featured_authorsをできる限りモデルの近くに置いておくことです。notifyメソッドは、Trashable concernの一部であるべきではありません。特に異なるサブグループに通知を送る傾向がある場合は、各モデルがそれぞれ独自にその責任を負うべきです。もう少し痛みの少ないやり方を見てみましょう。
# Concerns
module Trashable
extend ActiveSupport::Concern
included do
scope :existing, -> { where(trashed: false) }
scope :trashed, -> { where(trashed: true) }
end
def trash
update_attribute :trashed, true
end
end
module Authorable
has_many :authors
# モデル横断で著者に関連するその他の便利なメソッド。
# もし何もなければ、concern自体を廃止すべき。
end
# Models
class Song < ApplicationRecord
include Trashable
include Authorable
def featured_authors
authors.where(featured: true).where(region: 'Europe')
end
# ...
end
class Album < ApplicationRecord
include Trashable
include Authorable
def featured_authors
authors.where(featured: true)
end
# ...
endこのようなconcernsなら管理可能で、複雑すぎません。先ほど説明したnotify機能については省略しましたが、それはまた別の日の話題に取っておきましょう。
最終ボス
Railsの生みの親であるBasecampにとって、concernが他のconcernを参照することは完全に問題ないようです。DHHが以前ツイートで示したように:
コードのスクリーンショットを見ると、あなたは感嘆の声を上げるか、呆然とするかのどちらかでしょう。中間はない気がします。もし私がこのコードを編集する機会を得たら、「最終ボスとの決戦」のような気分になるでしょう。冗談はさておき、興味深いのは、どのconcernがどのconcernに依存しているかを示すコメントがある点です。ご覧ください。
# ...
include Subscribable # Depends on Readable
include Eventable # Depends on Recordables
# ...このようなコメントを残すのは役立ちますが、それでも怪しいことをするための下地になっています。特にコードベースに新しく参加した人にとっては顕著です。コードが持つすべての「落とし穴」を把握していない新人は、確実にconcernの負のスパイラルへと引きずり込まれかねません。
DHHはディスカッション内のコメントで、このようなコードについて語っています。返信のツイートでは、このコードベースで働く人々がどのようにこうしたconcernsと付き合うべきなのかと問いかけています。DHHの答えは、ドキュメントはほとんど整備していないが、採用頻度が低いためチーム全員がこれらに精通している、というものでした。
しかし、「経験豊富でコードベースをよく知るチームがいる」ことをconcerns使用の根拠にするのは奇妙で、説得力も弱いと言わざるを得ません。結局のところ、使うかどうかは感覚の問題なのでしょう。モジュールが提供する多重継承的なアプローチに心地よさを感じますか?それともコンポジション(合成)を好みますか?判断はあなた次第です。
まとめ
見てきたように、concernsとは、コードを抽出してDRYにするための便利な糖衣構文を提供するモジュールにすぎません。ツールボックスに他の有用な道具があるなら、すぐにconcernsに手を伸ばす必要はないかもしれません。ファイル添付の処理や、例で示したゴミ箱ロジックのような振る舞いは、モジュール(concerns)に抽出する良い候補になり得ます。
concernsやモジュール全般を扱う際の良し悪しを理解できたなら幸いです。完璧なコードなど存在しないことを忘れないでください。試してみて失敗したり成功したりしなければ、何が自分にとって良くて何が悪いのか、どうやって学べるでしょうか?
完璧な解決策は存在しません。この記事を通じて、Railsのconcerns流のやり方を理解していただけたなら嬉しいです。いつものように、自分の判断を信じ、メリットとデメリットの両方に注意を払いましょう。
それでは、次回お会いしましょう!
P.S. Ruby Magicの記事を公開と同時にお読みになりたい方は、Ruby Magicニュースレターを購読して、記事を見逃さないようにしてください!
-
Rails開発者が知るべきセキュリティ脅威:インジェクション攻撃の仕組みと対策を徹底解説
ユーザーデータを扱うアプリケーションを開発するなら、そのデータを確実に保護しなければなりません。しかし、セキュリティが初めての方にとっては、難しく、退屈で、複雑に感じられるかもしれません。 本記事は、Webアプリケーションにおける代表的なセキュリティ脆弱性と、それらがRails開発に与える影響を学ぶシリーズの第1回目です。道案内として活用するのは、OWASP Top 10(Web Application Security Risks)です。 OWASPとはOpen Web Application Security Project(オープンWebアプリケーションセキュリティプロジェクト)の略
-
Rails5でのAngularの使用
あなたは前にその話を聞いたことがあります。分散型で完全に機能するバックエンドAPIと、通常のツールセットで作成されたフロントエンドで実行されているアプリケーションがすでにあります。 次に、Angularに移動します。または、AngularをRailsプロジェクトと統合する方法を探しているだけかもしれません。これは、この方法を好むためです。私たちはあなたを責めません。 このようなアプローチを使用すると、両方の世界を活用して、たとえばRailsとAngularのどちらの機能を使用してフォーマットするかを決定できます。 構築するもの 心配する必要はありません。このチュートリアルは、この目的のた