ActiveModel::Attributes で定義した属性への `?` 付き呼び出しを実装する
?
サフィックス付きの属性メソッド呼び出しに応答してほしい
ActiveModel でも 例えば ActiveRecord
のモデルだと、定義された属性に対して object.foo
や object.foo = 1
のような更新や参照が可能なわけですが、更に object.foo?
といった形のメソッドも定義されています。
# 以下のテーブルが存在するとして…
# create_table :models do |t|
# t.foo :string
# t.bar :boolean
# end
#
class Model < ActiveRecord::Base
end
model = Model
model.foo = "HOGE"
model.bar = true
model.foo #=> "HOGE"
model.bar? #=> true
# boolean に限った定義というわけでなく、全属性で ? できる
model.foo? #=> true
一方、 ActiveModel
の場合だと ActiveModel::Attributes
を使って属性を定義するわけですが ?
サフィックス付きのメソッドは定義されません。
class User
include ActiveModel::Model
include ActiveModel::Attributes
attribute :name, :string
attribute :age, :integer
attribute :active, :boolean
end
user = User.new(name: "foo", age, 18, active: true)
user.name
=> "foo"
user.age
=> 18
user.active
=> true
user.active? #=> undefined method 'active?'
もちろん、自分で定義すれば使えるわけですが、このトピックは属性が増える都度これを書くという話ではありません。
これを ActiveModel でもいい感じに実装することについて考えます。
class User
# 省略...
def active? = active.present?
end
user = User.new(name: "foo", age, 18, active: true)
user.active? #=> true
ActiveRecord ではどうなっているか?
ActiveRecord の場合、 ActiveRecord::AttributeMethods::Query
にその実装があります。
そこでのポイントは、以下です。
attribute_method_suffix "?", parameters: false
attribute_method_suffix
は ActiveModel::AttributeMethods
で提供される定義です。
attriibuteなんちゃら
みたいな名前のメソッドを定義することができるものであり、これの仲間に attribute_method_prefix
(前置詞) や attribute_method_affix
(接辞. つまり prefix と suffix を同時に) があります。
class Foo
include ActiveModel::Model
include ActiveModel::Attributes
attribute_method_suffix "_double", parameters: false
attribute :bar, :string
attribute :baz, :integer
private
def attribute_double(attr_name)
value = attribute(attr_name)
value * 2
end
end
foo = Foo.new(bar: "HOGE", baz: 10)
foo.bar #=> "HOGE"
foo.bar_double #=> "HOGEHOGE"
foo.baz_double #=> 20
AttributeMethods::Query
を実装してみる
ActiveModel 版の というわけで ActiveRecord::AttributeMethods::Query
にならって、 ActiveModel で動作する実装を作ってみます。
以下みたいな形になります。
module QueryableAttribute
extend ActiveSupport::Concern
included do
include ActiveModel::Attributes
attribute_method_suffix "?", parameters: false
end
private
def attribute?(attr_name)
value = attribute(attr_name)
if value.respond_to?(:zero?)
!value.zero?
else
value.present?
end
end
end
この、QueryableAttribute モジュールを include します。
class Foo
include ActiveModel::Model
include ActiveModel::Attributes
include QueryableAttribute
attribute :bar, :string
attribute :baz, :integer
end
foo = Foo.new(bar: "HOGE", baz: 10)
foo.bar #=> "HOGE"
foo.bar? #=> true
foo.bar = ""
foo.bar? #=> false
これで、このモジュールによるサフィックスに ?
がついた実装を定義できました。
present?
ではダメ?
全部 以下は少しだけ詳細につっこんでいる内容です。
あまり興味がない場合は、「ActiveRecord での 属性名+? のメソッドは、値がゼロのときに偽を返すようになっている」とだけで OK です。
# ActiveRecord での 属性名+? のメソッドは、値がゼロのときに false を返します
model = Model.first
model.id #=> 1
model.id? #=> true
model.id = 0
model.id #=> 0
model.id? #=> false
で、先の attribute?
の実装ですが
def attribute?(attr_name)
attribute(attr_name).present?
end
とはしていません。
これは、挙動を ActiveRecord::AttributeMethods::Query
に寄せているためです。
ActiveRecord::AttributeMethods::Query
での判定は以下の query_cast_attribute
が大部分を担っています。
true
, false
, nil
の special case については early return していますね。
結果としては同じになるので自前で実装する場合はお好みで組み入れるも良いと思います。
case value
when true then true
when false, nil then false
以下の条件の箇所は少々ややこしいので端折っています。
if !type_for_attribute(attr_name) { false }
# ここ
(db:migrate を実施したときとか起因?) 実際の DB スキーマと、 ActiveRecord モデル自身が把握しているスキーマに乖離がある場合のフォローみたいなもののようです。
ActiveModel でこの条件を加味しないと具体的にどうなるか? ですが、以下のように attribute
の第2引数に型を指定しない場合などに微妙な挙動の違いが生まれます。
ただ、そこまで厳密に ActiveRecord と合わせきる必要がある? ⇒ なさそうだ。と考えて、これで良いとしています。 [1]
class Hoge
include ActiveModel::Model
include ActiveModel::Attributes
include QueryableAttribute
attribute :foo
attribute :bar, :string
attribute :baz, :integer
attribute :qux, :boolean
end
hoge = Hoge.new
hoge.foo = "0"
hoge.bar = "0"
hoge.baz = "0"
hoge.qux = "0"
[hoge.foo, hoge.foo?] #=> ["0", true] <<< ここは、件の条件を加味するならば false ということに
[hoge.bar, hoge.bar?] #=> ["0", true]
[hoge.baz, hoge.baz?] #=> [0, false]
[hoge.qux, hoge.qux?] #=> [false, false]
残りの条件が、以下です。
elsif value.respond_to?(:zero?)
!value.zero?
else
!value.blank?
end
おおよそ以下の挙動ということになります。
-
#zero?
に応答可能なオブジェクト (主に Integer などの数値)- ⇒
value.zero?
の評価結果の反対
- ⇒
- そうでない場合
- ⇒
value.blank?
の評価結果の反対 (つまりvalue.present?
と同義)
- ⇒
値がゼロであるならば ?
付き呼び出しは false
を返すということになります。
u1 = User.new(name: "bar", age: 0, active: false)
u1.age?
=> false
まとめ
- ActiveRecord と違って ActiveModel の attribute では
?
サフィックス付きの呼び出しには応答しません -
attribute_method_suffix
を使うと、それを実装する機能が実現できました - ActiveRecord での
?
サフィックス付き呼び出しは、実は値がゼロのときに false を返します
-
個人的にはこのあたりの複雑さをなくすために、
zero?
への応答は加味せずに全てのケースで単に#present?
で問う形のほうがよいのではないだろうかと思っています ↩︎
Discussion