Open3
Railsでロジックをどこに書くか
Railsでロジックをどこに書くか。散々議論されているテーマだが、実例を交えつつ、自分の現プロジェクトではどうするのか定めていきたい
ロジックとは以下のようなもの
- メインとなるModelの永続化前後で挟み込みたい処理(例:メール送信や操作履歴保存)
- DBへの永続化だけでなく、外部APIとの通信が必要なもの(例:会員登録時にfirebase authのアカウントも作成)
- コレクションの操作が必要なもの(例:商品を一括アップデート)
- 複数のオブジェクト間での相互にやりとりを行うもの(例:TBD)
ロジックを書く場所の候補は以下
- Controller(GraphQLならmutation, query)
- Modelのメソッド
- Modelのコールバック
- concern
- PORO(Service、UseCase層)
どのようなロジックであっても、以下の箇所には書きたくない
- Controller(GraphQLならmutation, query)
- 再利用性が低い
- テストを書きづらい(特にGraphQLだとクエリ書くのとレスポンスの検証が面倒)
- Modelのコールバック
- 意図しない場面でコールバックを実行してしまいがち(例:管理画面からの更新やアドホックなrails runnerで実行したくないコールバックが走ってしまった等)
- 例外は「どのような場合でも必ず実行したい」ケース(例:電話番号を入力時にはハイフンありなし両方許容したいが、DB保存時にはハイフンなしに統一したい)
- concern
- メソッド定義、影響範囲が追いづらい
- そもそも複数のモデルに跨った横断的関心事を切り出すもの(Taggableみたいな)なので、単純にModelがFatだからといってconcernに切り出すのはおかしい
- 横断的関心事であっても、別クラスにして委譲すれば代替できるケースがほとんど
したがって、
- Modelのメソッド
- PORO(Service、UseCase層)
のどちらに書くのかで迷うことが多いのだが、ここの判断が難しい。
ケースバイケースではあるのだが、ベースとなる指針は持ちたい。
ロジックの種類によって分けるとか。
具体的な例を上げて検討してみる
メインとなるModelの永続化前後で挟み込みたい処理(例:メール送信や操作履歴保存)
例:メール送信
- EC店舗への会員登録時にメール送信
- メールの種類は2つ。ユーザーへのメールと店舗へのメール
- ユーザー自身が登録した場合、ユーザー・店舗両方にメールする
- 店舗スタッフが代理で登録する場合、ユーザーへメールするかは任意に選択できる。店舗へのメールは必ず送信する
- ユーザーへのメール送信はトランザクション内。店舗へのメールはトランザクション外
# Modelのメソッドに書く場合
class User
def register(with_notification: false)
ActiveRecord::Base.transaction do
save!
if with_notification
UserMailer.send_registration_mail(self)
end
end
ShopMailer.send_user_registraion_mail(self)
end
end
# POROに書く場合
class UserRegistration
def initialize(user:, with_notification: false)
@user = user
@with_notification = with_notification
end
def save
ActiveRecord::Base.transaction do
@user.save!
if @with_mail_notification
UserMailer.send_registration_mail(@user)
end
end
ShopMailer.send_user_registraion_mail(@user)
end
end
このくらいだったらModelのメソッドにしたい気がする(POROに切り出すのは面倒だし、UserモデルみただけではUserに関する処理の全容が見えなくなる)のだが、もうちょい処理が増えてくるとPOROに切り出したくなる。(メソッドの長さくらいしか基準がない。。)
例:操作履歴保存
TBD