モデルにどんどん書いていっても後々大丈夫なのか

この記事について

本記事は、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) 

モデルに書いたときの困りごと

出典:toshimaru/blog – 銀座Rails#21で「Fat Modelの倒し方」を発表しました – Rails Modelの限界
https://blog.toshimaru.net/how-to-deal-with-fat-model/(参照:2022/09/08)

出典:toshimaru/blog – 銀座Rails#21で「Fat Modelの倒し方」を発表しました – Rails Modelはなぜ辛くなるのか?
https://blog.toshimaru.net/how-to-deal-with-fat-model/(参照:2022/09/08)

例えばコンテキストとバリデーションの関係。Railsで真っ先にぶつかる問題の一つですね。

出典:toshimaru/blog – 銀座Rails#21で「Fat Modelの倒し方」を発表しました – Rails Modelはなぜ辛くなるのか?
https://blog.toshimaru.net/how-to-deal-with-fat-model/(参照:2022/09/08)

これもよくありますね。そもそもActiveRecordは参照の方が得意な気がします。遅延書き込みなどの仕組みがないし

出典:toshimaru/blog – 銀座Rails#21で「Fat Modelの倒し方」を発表しました – Rails Modelはなぜ辛くなるのか?
https://blog.toshimaru.net/how-to-deal-with-fat-model/(参照:2022/09/08)

「モデルが肥大化する」というよりは「Rails Wayから(部分的に)自立する必要がある」ということが課題なのかもしれません。

さらに言えば、これはRails特有の問題ではありません。下記引用

出典:@ledsun blog – 人はFat Modelを恐れサービスを求め ドメインモデルは貧血に至る – 天国は善行で満ちている
https://ledsun.hatenablog.com/entry/2022/04/08/000748(参照:2022/09/08)

scopeの困りごと(記事紹介)

やはり、モデルとユースケースの癒着が問題とされているのだと思います。

出典: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として表現するのであれば問題ない、ということだと思います。

出典: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)
出典:Qiita (@Seiga in Classi株式会社) – Railsの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_ofdelegate など

 参考サイト

出典: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)

役に立ったらシェアしていただけると嬉しいです
  • URLをコピーしました!
  • URLをコピーしました!
目次