ActiveModelにおけるattributeの扱い

· 130 words · 1 minute read

※ この記事はRails5.2から導入されたActiveModel::Attributesについては記述していません。

はじめに

よくあるフォームの実装です。

class UserForm < ActiveModel::Model
  WITH_KIDS_OPTIONS = [['こどもの同席あり', true], ['こどもの同席なし', false]].freeze

  attr_reader :name, email

  validates :name, presence: true
  validates :email, presence: true, email: true
  validates :with_kids, presence: true, inclusion: { in: [true, false] }

  def save
    user = User.new(
      name: name,
      email: email,
      with_kids: with_kids
    )
    user.save
  end

  def with_kids
    ActiveRecord::Type::Boolean.new.cast(@with_kids)
  end
end

ActiveModel::ModelをincludeしてFormオブジェクトを定義しています。

このコードを見たとき、with_kidsメソッドを使って@with_kidsというインスタンス変数をキャストしている部分が何をやっているのか引っかかったので、丁寧に読んでみました。


attr_readerのはたらき

attr_readerは、Rubyのメソッドです。

内部的にインスタンス変数を定義し、クラスやモジュールにインスタンス変数を読み出すことを可能にします。

下記のコードは attr_reader :name, emailと同じ挙動になります。

Class UserFrom
  def name
    @name
  end
  def email
    @email
  end
end

はじめに出てきたコードでは、attr_readerでwith_kids属性だけ指定していません。

その代わり、自分でwith_kidsメソッドを定義しています。

つまり、with_kids属性のみ、attr_readerを使わずに独自のメソッドを使ってインスタンス変数を定義しているのです。

キャストを行う理由

では、なぜwith_kidsメソッドでキャストを行なっているのでしょうか?

キャストしている理由は、UserFormオブジェクトのwith_kids属性がString型に変換されてしまうのを防ぐことが目的です。

定数 WITH_KIDS_OPTIONSではvalueをBoolean型で指定しており、それを守りたいのです。

この処理の背景には、ActiveModelの存在があります。


ActiveModelについて

今回のコードでは、UserFormというクラスを定義しています。

これはRailsのActiveModel::Modelをincludeしています。

このように、FormなどのオブジェクトをActiveModelを使って切り出し、ActiveRecordを使ったモデルが大きくなりすぎないようにする、というリファクタリングのパターンがあります。

7 Patterns to Refactor Fat ActiveRecord Models

翻訳版はこちら


ActiveRecordのattribute機能

ActiveRecordにあってActiveModelにないものが、attributeの機能です。

ActiveRecordでは、attribute機能を使って属性の型の指定が簡単にできます。

class User < ApplicationRecord
  attribute :with_kids, :boolean, default: false
end

user = User.new
user.with_kids == false  

上のコードでは、with_kids属性に対し型とデフォルトの値を指定しています。

ActiveModelの場合は自分でこれをやってあげなければなりません。

そこで、with_kidsメソッド内でキャストしているのです。


まとめ

  • Rubyのattr_readerは内部的にインスタンス変数を定義する
  • ActiveRecordの代わりにActiveModelを使うモデル設計のパターンがある
  • ActiveModelはActiveRecordと違い、attributeのような機能を持っていない(※)
  • ActiveModelでattributeのようなことをしたいときは、attr_readerの外でインスタンス変数を定義して対処する(※)

※ 補足: Rails5.2から導入されたActiveModel::Attributesについて

Rails5.2からはActiveModel::Attributesという大変便利なものが使えるようになりました。

これをincludeして使うことで、ActiveRecodeのattributeと同じような機能を使えるようになりました。

ActiveModel::Attributesの導入についてのわかりやすい記事はこちらです。