この記事について
本記事は、2022年9月に実施した社内勉強会資料の内容をもとに、社外向けに再編集したものです。
記載の情報は執筆当時のものであり、最新の仕様やベストプラクティスとは異なる可能性があります。
実装にあたっては、必ず最新の公式ドキュメントをご確認ください。
はじめに
いくつかのケースで考えたことを共有したり、世の中の記事を紹介したりします。結論はありません。
Jobが読みにくくなってきたケース
Jobが読みにくくなってきたので、一部をモデルに書き直すことにしました。
例1
クレジットカードの有効期限判定について同じ計算判断を複数箇所で記述しており、読むたびに
「この不等号で正しかったか?」
「書き間違いはないか?」
「場合によって微妙に異なる計算をしているところはないか?」
などと気になる状態でした。
class PaymentJob < ApplicationJob
def perform
...
# creditcard_expiration_dateには月初日が入るかもしれない
if user.creditcard_expiration_date.end_of_month < time.end_of_month
...
end
いわゆる「デメテルの法則」を手掛かりにして、モデルにロジックを移動しました。
class PaymentJob < ApplicationJob
def perform
...
if user.creditcard_will_have_expired_at?(time.end_of_month)
...
end
class User < ApplicationRecord
...
def creditcard_will_have_expired_at?(time = Time.current)
# creditcard_expiration_dateには月初日が入るかもしれない
creditcard_expiration_date.end_of_month < time
end
例2
price = item.price - discount
pay(price)
負の金額を決算しないようにしなければなりません。
price = max[item.price - discount, 0].max
これを複数箇所に記述するのは辛いので
price = item.discounted_amount(discount)
class Item < ActiveRecord
def discounted_amount(discount)
raise '割引額が負です' if discount < 0
[price - discount, 0].max
end
当面はこれで良さそうですが、例外クラスも定義した方が良いかもしれません。
値クラス
例2のようなケースでは「値クラス」を使う、と言われることがあります。Railsではcomposed_of
を使って書けるようです。
出典:Sansan Tech Blog – composed_of を使って Rails で値オブジェクトを扱う
https://buildersbox.corp-sansan.com/entry/2020/04/08/110000 (参照:2022/09/08)
モデルに書いたときの困りごと
ここのキーワードとしては ユースケース。いろんなユースケースを1つのModelで表現しなければならないという状況が辛いと言えます。
出典:toshimaru/blog – 銀座Rails#21で「Fat Modelの倒し方」を発表しました – Rails Modelの限界
https://blog.toshimaru.net/how-to-deal-with-fat-model/(参照:2022/09/08)
1つのModelが複数の異なるユースケースに密結合して実装されるとき → ある条件やcontextに紐付いたValidation/Callback処理
出典:toshimaru/blog – 銀座Rails#21で「Fat Modelの倒し方」を発表しました – Rails Modelはなぜ辛くなるのか?
https://blog.toshimaru.net/how-to-deal-with-fat-model/(参照:2022/09/08)
例えばコンテキストとバリデーションの関係。Railsで真っ先にぶつかる問題の一つですね。
1つのフォームで複数のサブリソースが更新されるとき(フォームとModelが1対1で紐付かないとき) → 1つのModelを起点とした複数Modelを跨ぐトランザクション処理
出典:toshimaru/blog – 銀座Rails#21で「Fat Modelの倒し方」を発表しました – Rails Modelはなぜ辛くなるのか?
https://blog.toshimaru.net/how-to-deal-with-fat-model/(参照:2022/09/08)
これもよくありますね。そもそもActiveRecordは参照の方が得意な気がします。遅延書き込みなどの仕組みがないし
上述の限界は、Rails ModelとDBのテーブルが一対一で紐づくRailsの世界観に起因する限界と言えます。
出典:toshimaru/blog – 銀座Rails#21で「Fat Modelの倒し方」を発表しました – Rails Modelはなぜ辛くなるのか?
https://blog.toshimaru.net/how-to-deal-with-fat-model/(参照:2022/09/08)
「モデルが肥大化する」というよりは「Rails Wayから(部分的に)自立する必要がある」ということが課題なのかもしれません。
さらに言えば、これはRails特有の問題ではありません。下記引用
こう書くと、Ruby on Rails がドメインオブジェクトとリレーショナルデータベースのテーブルを対応させているから、サービスレイヤーが生まれるように思えるかもしれません。 2003年に「ドメインモデル貧血症」はすでに存在しています。 Ruby on Railsは2004年生まれです。
出典:@ledsun blog – 人はFat Modelを恐れサービスを求め ドメインモデルは貧血に至る – 天国は善行で満ちている
https://ledsun.hatenablog.com/entry/2022/04/08/000748(参照:2022/09/08)
scopeの困りごと(記事紹介)
やはり、モデルとユースケースの癒着が問題とされているのだと思います。
二つのControllerで同じscopeが利用されているパターンを考えてみましょう …(略)… するとなんということでしょう、Aさんの預かり知らぬところでバグが発生してしましました。それは同じscopeを使っていたBさんの作成したSome2Controllerでした…fin
出典:Qiita (@Seiga in Classi株式会社) – Railsのscopeのアンチパターンとその解消法
https://qiita.com/Seiga/items/e71e9497a395fe61102f(参照:2022/09/08)
これもよくありますね。ここでは対策として「クエリオブジェクト」をお勧めされています。
オススメはクエリオブジェクトを作ってそちらにクエリ組み立てを委譲 する方法です。こうすることで暗黙的な結合を防ぎつつ、ユースケース依存の複雑なクエリを組み立てることができます。
出典:Qiita (@Seiga in Classi株式会社) – Railsのscopeのアンチパターンとその解消法
https://qiita.com/Seiga/items/e71e9497a395fe61102f(参照:2022/09/08)
module Some1
class Some1ListQuery
class << self
def call(param)
SomeModel.published
.eager_load(SomeBelongsToModel)
.where(some_belongs_to_model: {someparam: param})
.order(created_at: :desc)
end
end
end
end
::Some::Some1ListQuery.call(param)
「ドメイン(対象業務上の概念・知識)」を、特定のユースケースに依存せずscopeとして表現するのであれば問題ない、ということだと思います。
scopeを使ってドメインを表現する集合を引っ張ってくるときに、どんなカラムにどんな絞り込みをかけるのかは抽象化することができます。これはscopeのメリットです。
出典:Qiita (@Seiga in Classi株式会社) – Railsのscopeのアンチパターンとその解消法
https://qiita.com/Seiga/items/e71e9497a395fe61102f(参照:2022/09/08)
scopeの命名ではドメインを表現する抽象化された集合の名前をちゃんと考えて命名しましょう。 最悪弊社VPoTに切腹させられます
出典:Qiita (@Seiga in Classi株式会社) – Railsのscopeのアンチパターンとその解消法
https://qiita.com/Seiga/items/e71e9497a395fe61102f(参照:2022/09/08)
それに対し、
出典:Qiita (@Seiga in Classi株式会社) – Railsのscopeのアンチパターンとその解消法published
、のようにドメインとしての抽象化ができているscopeもあります。 これは「公開済み」と言うドメインを表現するscopeになるので変更耐性も強く、使いまわしても悪い作用が起こることは少ないです。
https://qiita.com/Seiga/items/e71e9497a395fe61102f(参照:2022/09/08)
※ちなみに、「scopeはコントローラでチェインしないほうがいい(チェインもモデル内で完結させる)」という意見もあ流ようです。
「メソッドの変更耐性が強い」とは?
そのメソッドが変更される理由が限定的であるということを意味しているのだと思います。
たとえば――
- 業務に関する知識の理解が不十分だった
- 実際の業務内容が変更された
Rails Way ではうまくカバーできないようなケースへの対応(記事紹介)
Rails Way ではうまくカバーできないようなケースへの対応については、先ほどの記事で解説されています。
出典:toshimaru/blog – 銀座Rails#21で「Fat Modelの倒し方」を発表しました
https://blog.toshimaru.net/how-to-deal-with-fat-model/(参照:2022/09/08)
まとめ
・ドメイン(対象業務上の概念・知識)は、ためらわずにモデルに書いてしまってよさそうです。そのとき、特定のユースケースに密結合しないように注意しましょう。
・ユースケースを整理して扱おうとするとき、Railsを超えた設計の話になることもあるので気をつけましょう。
・意外とRails がカバーしようとしてくれている、ということもあります。composed_of
や delegate
など
参考サイト
出典:Sansan Tech Blog – composed_of を使って Rails で値オブジェクトを扱う
https://buildersbox.corp-sansan.com/entry/2020/04/08/110000 (参照:2022/09/08)
出典:toshimaru/blog – 銀座Rails#21で「Fat Modelの倒し方」を発表しました
https://blog.toshimaru.net/how-to-deal-with-fat-model/(参照:2022/09/08)
出典:Qiita (@Seiga in Classi株式会社) – Railsのscopeのアンチパターンとその解消法
https://qiita.com/Seiga/items/e71e9497a395fe61102f(参照:2022/09/08)