読者です 読者をやめる 読者になる 読者になる

パーフェクトRails 9章 より実践的なモデルの使い方(9-1〜9-2)

パーフェクト Ruby on Rails

パーフェクト Ruby on Rails

Rails環境について

  • 本の環境 : Rails4.1.1
  • 本記事執筆時の環境 : Rails4.2.3

9-1. アーキテクチャパータンから見るRails

Skinny Controller, Fat Model

モデルを厚く、コントローラーを薄く

  • コントローラーをシンプルに → ビジネスロジックがわかりやすくなる
  • データ加工処理はモデルに担当させる

用語の整理

Rails の model層は、ドメインモデル、ドメインロジックをコードとして形にするレイヤー

  • controller は?

Railsでは、ドメインモデルと、実際のRDBマッピングの為に、 AcitveRecored を使用

アクティブレコードパターン

ドメインモデルに関わるロジックとデータの永続化処理を1つのクラスにまとめてカプセル化する手法

AcitveRecoredの落とし穴と回避方法

modelの誇大化

  • バリデーションやらコールバックやらドメインロジックやら、modelに書くこと多いよね...

対策

  • 責任を分割した小さなクラスに分ける
    • バリデーションとコールバックの抽出
  • RDBに依存しないモデルクラス
  • システムの機能の一部をモデルの外に抽出する

9-2. バリデーションとコールバックの抽出

メリット

  • 責任の分割
  • コードの可読性
  • テストがわかりやすくなる
    • モデルクラスはRDSと密接に関連しているので、テスト用のモデルを用意するのが大変
    • テストの本質と関係ないところにコストが掛かるのは健全ではない

どういう時分ける?

  • テストの為の事前条件を満たすのが困難な場合
  • 同じコールバックや、バリデーションが複数のモデルで実装されている場合
  • 業務知識として重要な場合
    • 「独立した概念である」ということをコード上で表現する
    • 業務上の概念と、設計上の概念はできるだけ一致するべき

コールバックの抽出

  • コールバックにオブジェクトを渡せる
  • そのオブジェクトは コールバックと同じ名前のメソッド を持つ
class BankAccount < ActiveRecord::Base
  before_save      EncryptionWrapper.new
  after_save       EncryptionWrapper.new
  after_initialize EncryptionWrapper.new
end

class EncryptionWrapper
  def before_save(record)
    record.credit_card_number = encrypt(record.credit_card_number)
  end

  def after_save(record)
    record.credit_card_number = decrypt(record.credit_card_number)
  end

  alias_method :after_initialize, :after_save

  private
    def encrypt(value)
      # Secrecy is committed
    end

    def decrypt(value)
      # Secrecy is unveiled
    end
end

参考

バリデーションの抽出

ActiveModel::EachValidator

個別の属性を検証するためのカスタムバリデータ

  • ActiveModel::EachValidator を継承
  • validate_each メソッドを持つ
    • record : 検証対象のレコード
    • attribute : 検証対象のカラム名(下記の場合、:email)
    • value : 検証対象の値
  • EmailValidator ならば、validates :user_email, email: true という形で呼び出される
class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
      record.errors[attribute] << (options[:message] || "は正しいメールアドレスではありません")
    end
  end
end
 
class Person < ActiveRecord::Base
  validates :email, presence: true, email: true
end

ActiveModel::Validator

レコード全体を検証するためのカスタムバリデータ

  • ActiveModel::Validator を継承
  • validate メソッドを持つ
    • 引数にレコードが入る
  • validates_with メソッドで呼び出される
class MyValidator < ActiveModel::Validator
  def validate(record)
    unless record.name.starts_with? 'X'
      record.errors[:name] << '名前はXで始まる必要があります'
    end
  end
end
 
class Person
  include ActiveModel::Validations
  validates_with MyValidator
end
  • validates_with にHashでオプションを渡せる
    • Validatorクラスでは、optionsメソッドでオプションを取り出せる

Validatorクラスの注意点

Validatorクラスには一つ注意点があります。Validatorクラスは通常一度しかインスタンス化されず、そのオブジェクトをずっと使い回すことになります。変にインスタンス変数とかを利用するとずっと残ってしまうので利用する場合は、意図せず変更されないように注意しましょう。

抽出したクラスってどこに置くのが良いかな?

  • 1つのモデルでしか使わないクラス : /app/model/bank_account/ みたいにモデル名のディレクトリの下
  • 複数のモデルで使うクラス : /app/model/validator/ みたいに、役割名のディレクトリの下

かなぁ

TODO

Skinny Controller, Fat Model についてもう少し理解を深める

参考