RailsのConcernについて調べた内容を整理しておく。 また、この記事ではモデルのConcernについてのみ扱うことにする。
RailsにおけるConcernとは、モデルやコントローラの関心事をまとめ、Mixinとして組み込むRubyモジュールである。通常のRubyモジュールとして実装できるが、includedやクラスメソッドの定義、モジュール間の依存関係を扱う場合にはActiveSupport::Concernを利用できる。
app/models/concerns ディレクトリが存在するが、Concernの配置・運用方針についてまとまった公式ガイドはない。
そのため、実装者によってConcernの解釈や使い方が分かれることも多く、統制をとるのが難しい部分でもある。
Concernの全体像としては、以下の記事がよくまとまっているので参考にするとよい。
参考: Concerns in Rails: Everything You Need to Know
前述したように、Concernの配置・運用方針についてまとまった公式ガイドはないが、DHHの発言や37signalsのブログ記事、once-campfireなどから使用方針を読み取ることができる。それらを参考にしながら使用方針や活用する上での考え方について書いていくことにする。
moduleとして名前をつけ、MixinすることでモデルのAPIとして利用できる)37signalsのブログ記事やonce-campfireからConcernは以下2つに分けて運用されていることがわかる。
app/models/concernsapp/models/model名app/models/concernsに入れる
モデルクラス共通の汎用性のある関心事を配置する。 特定モデルのConcernが複数のモデルから参照されるようになったものや、汎用性のある技術的な関心事が配置される。
HEYでの例をDHHが紹介している。
module Eventable
extend ActiveSupport::Concern
included do
has_many :events, as: :eventable, dependent: :destroy
...
class Access < ApplicationRecord
include Eventable
class Clip < ApplicationRecord
include Eventable
この例では、モデルの関連を切り出し、複数モデルでMixinしている。(技術的な関心事であるとも言えるかもしれない) DHHの説明では、技術的な関心事に加えて
上記のようなユースケースにも、モデル共通のConcernを活用すると言っている。
AOPが技術的な関心事の切り出し方の参考になる。例えば、ロギングやパフォーマンス計測、トランザクション処理、例外処理などが挙げられる。
RailsのConcernでは、モデルのコールバックに共通処理を登録する使い方がある。例えば、複数モデルに共通するイベント記録や監査ログをConcernにまとめ、Active Recordのコールバックとして登録できる。
class Todo < ApplicationRecord
# Other todo implementation
# ...
include TodoEventTracking
end
module TodoEventTracking
extend ActiveSupport::Concern
included do
has_many :events
before_create :track_creation
end
private
def track_creation
Rails.logger.info("Create todo event. ID: #{id}")
end
end
参考: Concerning のサンプルコード
once-campfireを参考にする。
以下は、Userモデルが担うアバターに関する役割を、Concern(User::Avatar)として分離した例である。
class User < ApplicationRecord
include Avatar, Bot, Mentionable, Role, Transferable
...
end
module User::Avatar
extend ActiveSupport::Concern
included do
has_one_attached :avatar
end
class_methods do
def from_avatar_token(sid)
find_signed!(sid, purpose: :avatar)
end
end
def avatar_token
signed_id(purpose: :avatar)
end
end
DDDで扱われる「文脈によってモデルの意味が限定される」という観点から、特定モデルのConcernを理解してみる。
たとえば顧客という用語ひとつとっても、複数の意味があるはずだ。ユーザーがカタログを見ているとき「顧客」とユーザーが注文をするときの「顧客」とは違った意味になる。 なぜか。カタログを見ているときの「顧客」は、まだ商品を購入する前の段階だ。固定客集めや取扱商品数、割引、発送方法などを考えるコンテキストにいる。一方、注文時点での顧客は、その意味が限定される。例えば、名前・発送先・請求先・注文額・支払い方法などが関わってくるだろう。
引用: 実践ドメイン駆動設計
Userモデルは、アバターを持つ主体、Botとしての振る舞い、メンションの対象など、利用される文脈によって異なる役割を担う。User::Avatar、User::Bot、User::MentionableなどのConcernは、Userが文脈ごとに担う役割をコード上で明示する単位として解釈できる。
ここでは、各ConcernをDDDにおける境界づけられたコンテキストそのものとして扱うのではなく、単一のモデルが担う文脈依存の役割をまとめる単位として捉えている。役割ごとにインターフェースや処理をまとめることで、モデルが提供するAPIを保ちながら可読性を向上させている。
Code I like (III): Good concerns より、37signalsではクラスによる委譲とConcernを対立的なものではなく、うまく組み合わせることで責務は切り分けつつドメイン指向でシンプルなコードを実現しているとのこと。
以下は上記のブログのコードを引用。
class Account < ApplicationRecord
include Closable
end
module Account::Closable
def terminate
purge_or_incinerate if terminable?
end
private
def purge_or_incinerate
eligible_for_purge? ? purge : incinerate
end
def purge
Account::Closing::Purging.new(self).run
end
def incinerate
Account::Closing::Incineration.new(self).run
end
end
Account::Closing::Purging, Account::Closing::Incinerationなどの複雑なロジックはクラスを活用してカプセル化や継承による再利用をしつつ、terminateメソッドだけを呼び出し側に提供する。
そうすると、アカウントの停止について、以下のようなインターフェースを実現することができる。
account.terminate # アカウントを停止する意図が分かりやすい
Serviceオブジェクトを作成するよりも、よりドメイン指向なインターフェースとなる。
AccountTerminationService.new(account).run # やや冗長
つまり、委譲による責務の分離によりアカウントの停止についてのロジックを扱うFatなモデルとなることを避けつつ、Concernをglueとして使うことでドメイン指向で簡潔なインターフェースをモデルに実装することができる。