ルビーオンレイルザーの皆さん、 Fat モデル対策やってますか。 Fat モデル対策と言えば Concern ですね。 app/models/concerns/
ディレクトリに module を置いてモデルに include させるというアレです。
しかしただ module を作って Fat モデルのコードを移動し、元のモデル側に include させるだけでは結局モデルのインスタンスに生えるメソッドの数に変わりはないので臭いものに蓋をしてるだけになります。 Rubocop の Metrics/LineLength
警告を逃れるためだけの module 乱立はあんまり意味がないでしょう。間違って別の module で同名のメソッドを定義してしまい意図しない挙動になってしまうことも考えられます。
最近自分がやってるのは、 include される module に定義するメソッドはせいぜい一つか二つにして、このメソッドから別クラス( Plain Old Ruby Object)に定義したメソッドを呼び出す(委譲する)というものです。モデルに得体の知れないメソッドが増えないので便利。
例えば以下のようなモデルがあるとします。
app/models/entry.rb
app/models/comment.rb
両方ともレコードの新規登録があったときに通知を行いたい。共通の処理なので Notifiable
モジュールを作ってそれを Entry
モデルと Comment
モデルでそれぞれ include しましょう。ここまでは皆さんよくやると思います。
app/models/concerns/notifiable.rb
module Notifiable
private
def notify
# do something
end
end
class Entry < ApplicationRecord
include Notifiable
has_many :comments
after_commit :notify, on: :create
end
class Comment < ApplicationRecord
include Notifiable
belongs_to :entry
after_commit :notify, on: :create
end
しかし Entry と Comment では通知内容が異なるので単純に #notify
メソッドを callback で実行すればよいというわけではない。通知用パラメーターを生成する処理をモデルに書くとよいのですが、そういうのを繰り返した結果が Fat モデル地獄なので通知内容を生成するクラスを別に作ります。こんな感じ。
app/models/concerns/notifiable/entry_notification.rb
app/models/concerns/notifiable/comment_notification.rb
Base クラスを作って共通処理をまとめ、継承させると便利でしょう。
app/models/concerns/notifications/base_notification.rb
module Notifiable
class BaseNotification
def initialize(object)
@object = object
end
def perform
NotificationJob.perform(notification_params)
end
end
end
module Notifiable
class EntryNotification < BaseNotification
def notification_params
{
recipient_ids: @object.subscriber_ids,
title: @object.title
}
end
end
end
module Notifiable
class CommentNotification < BaseNotification
def notification_params
{
recipient_ids: @object.thread_joiner_ids,
title: @object.body.truncate(25)
}
end
end
end
Notifiable モジュールはこんな感じになります。
module Notifiable
private
def notifiy
notification.perform
end
def notification
"#{self.class}Notification".constantize.new(self)
end
end
図にするとこんな感じ。
この Notifiable モジュールを include しても Model には #notify
メソッドと #notification
メソッドしか追加されず、通知処理の実装をモデルから分離することができます。 Entry
クラスも Comment
クラスも #notify
メソッドより先のことは何も気にしなくてよくなる。リソースが追加されたときに #notify
メソッドを実行することだけに責任を持てばよいし、通知を飛ばすという処理自体は Notifiable::EntryNotification
と Notifiable::CommentNotification
クラスの責任になります。
私はこれで Fat モデルのコードを concerns ディレクトリにしまい込んで臭いものに蓋をするような対応におさらばしました。よろしければお試し下さい。またもし他に良い方法をご存じであれば教えて下さい。