たばりばりスタイル

たばりばりスタイル

バリバリバリ⚡︎

RailsでModelになるべく自前メソッドを書かないようにしている

Rails を業務で使うようになって、数百行ある Fat なモデルの保守をすることも増えました。
Fat なモデルでは、同じようなメソッドが複数存在したり、使用されていないメソッドが存在したりなどカオスな状況が起こりやすいと思います。

行数が短いモデルが必ずしも ''正義'' だとは思いませんが、短い方がメンテが楽だと思っており、コードを書くときはなるべく行数が少なくなるように努力をしています。

今回は、モデルの行数を節約するために実践している ''Rails の標準機能を使うようにして、自前でメソッドを作らない方法'' をまとめたいと思います。

has_many で絞り込みを使う

関連オブジェクトの絞り込みをしたい場合、自前メソッドを用意するより has_many で絞る方が行数を節約できます。

# 自前メソッド例
class User < ApplicationRecord
  has_many :posts

  def recently_posts
    posts.order(created_at: :desc)
  end
end

# has_many で絞り込みを使う
class User < ApplicationRecord
  has_many :recently_posts, -> { order(created_at: :desc) }, class_name: 'Post'
end

scope を使う

関連オブジェクトやモデルのクエリを用意したい場合、自前クラスメソッドを用意するより scope を使う方が行数を節約できます。

# 自前メソッド例
class User < ApplicationRecord
  class << self
    def matches_email(email)
      where(email: email)
    end
  end
end

# scope を使う
class User < ApplicationRecord
  scope :matches_email, ->(email) { where(email: email) }
end

delegate を使う

関連オブジェクトのメソッドを利用したい場合、自前メソッドを用意するより delegate で委譲した方が行数を節約できます。

# 自前メソッド例
class Post < ApplicationRecord
  belongs_to :user

  def user_first_name
    user.first_name
  end

  def user_last_name
    user.last_name
  end
end

# delegate を使う
class Post < ApplicationRecord
  belongs_to :user

  delegate :first_name, :last_name, to: :user, prefix: true
end

composed_of を使う

年齢などの計算ロジックが必要な値を利用する場合、自前メソッドより値用のクラスを分けて composed_of を利用する方が行数を節約できます。

# 自前メソッド例
class User < ApplicationRecord
  def age
    current = Time.current.strftime('%Y%m%d').to_i
    birthed = birthed_on.strftime('%Y%m%d').to_i
    (current - birthed) / 10_000
  end
end

# composed_of を使う
class User < ApplicationRecord
  composed_of :age, mapping: [:birthed_on]
end

カスタムバリデーションクラスを使う

自前のバリデーションが必要な場合、自前メソッドを用意するよりカスタムバリデーションクラスを用意した方が行数を節約できます。

# 自前メソッド例
class Tweet < ApplicationRecord
  CONTENT_MIN_LENGTH = 1
  CONTENT_MAX_LENGTH = 140

  validate :content_cannot_violate_length_restriction

  private
  def content_cannot_violate_length_restriction
    return if self.content.blank?
    unless self.content.length >= CONTENT_MIN_LENGTH && self.content.length <= CONTENT_MAX_LENGTH
      errors.add(:content, 'Violates length restriction')
    end
  end
end

# カスタムバリデーションクラスを使う
class Tweet < ApplicationRecord
  validates_with TweetLengthValidator
end

最後に

行数が短いモデルが必ずしも ''正義'' だとは思っていないので、これらの方法がどの状況でも良い選択になるかといえば違うと思います。
ただ、「Rails の機能を使って短く書く」をチーム内で意識すれば、コードにも統一感が出てメンテの面でもいい感じになりそうだなと思っています。

以上です。