There's an echo in my head

日々のメモ。

acts_like?でduck typing

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

とはいえここでいうbakenekonekoと同じように振る舞うことができるのだから、それを判別したい。 ここで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?を呼んでいるだけなので、上記のような心配をする必要が無い。

このブログに出てくるコードスニペッツは、引用あるいは断りがない限りMITライセンスです。