1年ぐらい前に社内ブログにメモしてた内容をまんま転載。
はて、これはduck typingなのか?
概要
Object#acts_like?
を使うとクラスの異なるオブジェクトが同じ振る舞いをするかどうかの判定が手軽にできるようになる。
具体例
ActiveSupportではTimeとDateTime、ActiveSupport::TimeWithZoneに共通のメソッドを実装し、それらが入れ替わってもうまく動くようになっている。
これを実装するにあたってにTimeっぽく振る舞うかどうかを判定するためにacts_like?
を使っている。
require "active_support/all"
# Timeっぽく動くやつら
Time.now.acts_like?(:time) #=> true
DateTime.now.acts_like?(:time) #=> true
Time.zone.now.acts_like?(:time) #=> true
# Timeじゃないやつ
Date.today.acts_like?(:time) #=> false
仕組み
すごく単純で、acts_like_time?
が実装されていればacts_like?(:time)
がtrueになる。
require "active_support/core_ext/object/acts_like"
class Bakeneko
def acts_like_human?
true
end
end
Bakeneko.new.acts_like?(:human) #=> true
ちなみに実装されているかだけを見ているので、その返り値は見られない。
でもわかりやすさのためにもtrueを返すのが良いと思う。
使いドコロ
例えばSimpleDelegatorの継承と組み合わせると便利。
通常、SimpleDelegatorを継承したクラスはデリゲート先のクラスとはis_a
の関係にならない。
require "delegate"
class Neko; end
class Bakeneko < SimpleDelegator; end
neko = Neko.new
bakeneko = Bakeneko.new(neko)
neko.is_a?(Neko) #=> true
bakeneko.is_a?(Neko) #=> false
とはいえここでいうbakeneko
はneko
と同じように振る舞うことができるのだから、それを判別したい。
ここでacts_like?
の出番。
require "active_support/core_ext/object/acts_like"
class Neko
def acts_like_neko?
true
end
end
class Bakeneko < SimpleDelegator
# SimpleDelegator側がオリジナル側のrespond_to?も見てくれるので
# 実際はこのケースでは別途定義する必要はない
def acts_like_neko?
true
end
end
neko.acts_like?(:neko) #=> true
bakeneko.acts_like?(:neko) #=> true
このようにすることで、acts_like?
を使うことで異なるクラスでも同じ振る舞いをするオブジェクトの判定ができるようになる。
それ以外で言うと
ひとつのクラスが複数の振る舞いを持つ場合にも対応できる。
例えばDateTimeはDateのようにもTimeのようにも振る舞える。
DateTime.now.acts_like?(:date) #=> true
DateTime.now.acts_like?(:time) #=> true
振る舞いの判定がクラスとは別個に行えるので自由度が増える。
(とはいえ全く違う役割をもたせるのはクラス設計的にどうなのという感じではある)
FAQ
acts_like_xxx?
を直接呼べばよいのでは
レシーバに実際にそのメソッドが定義されていない場合にエラーになるのでNG。
require "active_support/all"
Date.today.acts_like_time? #=> NoMethodError
一方でacts_like?
は内部的にrespond_to?
を呼んでいるだけなので、上記のような心配をする必要が無い。