【パーフェクトRails】バリデーションをクラスに分離する

目的

パーフェクトRuby on Railsの9章の9−2「複雑なバリデーションとコールバックを整理する」を読んでいて 「バリデーションをクラスに分離する」について、知らないことがいくつかあったのでまとめます。

パーフェクト Ruby on Rails

パーフェクト Ruby on Rails

バリデーションをクラスに分離するのは必要?

メールとか氏名とか、プロジェクトで要件が共通になると思うので積極的にやって良いと思う。 Open Source Rails でもapp/validatorsは結構見かける。

どうやってやるのか?

例によってサンプルを見るのがわかりやすいと思います。

EachValidatorを使う

# app/validators/email_validator.rb
class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    record.errors.add(attribute, 'is not an email') unless value =~ /.+@.+/
  end
end

上記の通りActiveModel::EachValidatorを継承してvalidate_eachを定義すればいい。
呼ぶときは以下のように呼ぶ。

class Person
  include ActiveModel::Validations
  attr_accessor :email, :name

  validates :email, email: true
end

validates :email, email: trueは「emailというフィールドに対して、EmailValidator#validate_eachでチェックしてください。」というような解釈で良いと思う。EmailValidatorを見ると引数をがどのように渡ってくるかがわかると思う。

Validatorを使う

# app/validators/multi_presence_validator.rb
class MultiPresenceValidator < ActiveModel::Validator
  def validate(record)
    record.errors.add(:base, 'bust be presence') if options[:attributes].all? { |c| record.send(c).blank? }
  end
end

ActiveModel::Validatorを継承してvalidateメソッドを定義する。
呼び出し方は以下。

class Person
  include ActiveModel::Validations
  attr_accessor :email, :name

  validates_with MultiPresenceValidator, attributes: [:email, :name]
end

validates_with MultiPresenceValidatorは見たとおりMultiPresenceValidator#validateでバリデーションをする。

attributes: [:email, :name]という引数に関しては、ActiveModel::Validatorインスタンスからoptions[:attributes]とすればさわれるようになっている。

validates_with Validator1, Validator2と書くと両方のバリデーションを付け加えることができる http://apidock.com/rails/ActiveModel/Validations/ClassMethods/validates_with

EachValidatorとValidatorの違い

EachValidatorは、上のサンプルの呼び出し方にある通り、1つのフィールドをチェックするやりかた(もちろん複数のフィールドをチェックするように書くことも出来なくはない)になる。

Validatorは複数のフィールドをチェックするのに使う。(こちらも1つのフィールドだけをチェックチェックするのは問題なく出来る。)

EachValidatorValidatorのサブクラスになっている。

その他

これらの自作バリデーションや標準のバリデーションに関しては、モデルクラスごとにオブジェクトを持つ。インスタンス単位ではない。したがって、状態を持たないようにすること。

まとめ

  • app/valiadtorsディレクトリはわりと一般的になっているので、プロジェクトたちあげのときに追加してバリデーションのクラス化を促すのが良いと思う。

  • 1フィールドのときは、ActiveModel::EachValidatorを継承してvalidate_eachメソッド定義

  • 複数フィールドのときは、ActiveModel::Validatorを継承してvalidateメソッド定義

  • PresenceValidatorsの実装とか読むといいと思う