Open3

Railsでロジックをどこに書くか

maeharinmaeharin

Railsでロジックをどこに書くか。散々議論されているテーマだが、実例を交えつつ、自分の現プロジェクトではどうするのか定めていきたい

ロジックとは以下のようなもの

  1. メインとなるModelの永続化前後で挟み込みたい処理(例:メール送信や操作履歴保存)
  2. DBへの永続化だけでなく、外部APIとの通信が必要なもの(例:会員登録時にfirebase authのアカウントも作成)
  3. コレクションの操作が必要なもの(例:商品を一括アップデート)
  4. 複数のオブジェクト間での相互にやりとりを行うもの(例:TBD)

ロジックを書く場所の候補は以下

  1. Controller(GraphQLならmutation, query)
  2. Modelのメソッド
  3. Modelのコールバック
  4. concern
  5. PORO(Service、UseCase層)
maeharinmaeharin

どのようなロジックであっても、以下の箇所には書きたくない

  • Controller(GraphQLならmutation, query)
    • 再利用性が低い
    • テストを書きづらい(特にGraphQLだとクエリ書くのとレスポンスの検証が面倒)
  • Modelのコールバック
    • 意図しない場面でコールバックを実行してしまいがち(例:管理画面からの更新やアドホックなrails runnerで実行したくないコールバックが走ってしまった等)
    • 例外は「どのような場合でも必ず実行したい」ケース(例:電話番号を入力時にはハイフンありなし両方許容したいが、DB保存時にはハイフンなしに統一したい)
  • concern
    • メソッド定義、影響範囲が追いづらい
    • そもそも複数のモデルに跨った横断的関心事を切り出すもの(Taggableみたいな)なので、単純にModelがFatだからといってconcernに切り出すのはおかしい
    • 横断的関心事であっても、別クラスにして委譲すれば代替できるケースがほとんど

したがって、

  • Modelのメソッド
  • PORO(Service、UseCase層)

のどちらに書くのかで迷うことが多いのだが、ここの判断が難しい。
ケースバイケースではあるのだが、ベースとなる指針は持ちたい。
ロジックの種類によって分けるとか。

maeharinmaeharin

具体的な例を上げて検討してみる

メインとなる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