Railsのモデルの更新系メソッドをどう書くか

Railsでアプリを作っていて、モデルの更新系メソッドをどう定義するのが良いのだろうかと試行錯誤していたのだが、もうこれでいいんじゃないかな、というパターンが固まってきたのでメモっておく。

処理とエラーチェックの分離

メソッド定義内ではエラーチェックをせず、すべてがうまくいっているかのようなコードを書く。 状態を変更して保存し、自分自身を返す、というのがパターン。

エラーチェックはもっぱらバリデーションでおこなう。 定義したメソッドの実行時に必要なチェックが全て実行されるようにバリデーションを定義する。

# メインの処理
def do_something
  self.status = 'new_status'
  save
  self
end

# do_something実行時に実行されるようバリデーションの条件を指定する
validate :can_be_new_status, if: 'status_changed? && new_status?'

# バリデーションの定義
def can_be_new_status
  unless can_be_new_status?
    errors.add :status, :cannot_be_new_status
  end
end

バリデーションは必要に応じていくらでも増やせる。 チェック内容すべてを一つのバリデーションに詰め込んでもいいし、それらを個別のバリデーションに分けてもいい。 はじめはひとつのバリデーションに詰め込んでおいて、複数のメソッドで共通のチェック内容が出てきたら分離すればよいかと思う。

呼び出し側でのエラーチェック

メソッドの呼び出し側で処理の成否を確認するには、返されたオブジェクトのvalid?やerrors.any?、persisted?などのメソッドを使う。

何がうれしいのか

この方式だと、検出したいエラーの種類を増やしたいときにメソッド本体を修正する必要がなく、単にバリデーションを追加していけばよい。 戻り値として返されるオブジェクトにはバリデーションで引っかかった項目すべての情報が含まれているので、何がダメだったのかこれ以上ないほどに詳しい情報が含まれている。

これがメソッド本体でif文をならべて最初に引っかかったチェックでエラーを返す方式だと、たまたま最初に引っかかった問題点しかわからないので、その問題を修正して再度処理を実行しても、今度はまた別の問題で引っかかる可能性がある。

エラーを返す方法としてBool値や例外を使うことも可能だが、Bool値だとエラーの詳細がわからないし、例外を使う方法だと、そもそもどんな例外を定義するか考えたり、一貫性のある例外の使い方をするよう気をつけたりする必要があって、面倒くさい。

メソッド定義とエラーチェックが分離しているのはコードを追うときに若干わかりづらくなるかもしれないけど、コードはスッキリするし、エラーの検出も網羅的になるので、利点は大きい。

エラーを検出するのにいちいちバリデーションを追加するのは手間だと感じるかもしれないが、成功したか失敗したかぐらいの判別しかできない中途半端なエラー情報を返すメソッドを定義しても、後々自分が不便な思いをして修正するハメになりがちだ。 それだったら、決まりきったパターンとしてメソッドとバリデーションを定義して、バリデーション通過後の自分自身を返すメソッドを黙々と書いたほうが結局は時間の節約にもなるんじゃないかな。