- Ruby / Rails関連
[Rails5] Active Support Core ExtensionsのString#acts_like_string?
こんにちは、hachi8833です。Active Support探訪シリーズ、今回はString#acts_like_string?
にお邪魔します。
今回のメソッド
- メソッド:
String#acts_like_string?
- ディレクトリ配置: https://github.com/rails/rails/blob/5-0-stable/activesupport/lib/active_support/core_ext/string/behavior.rb
条件
- Railsバージョン: 5-0-stable
- Rubyバージョン: 2.3.3
string/behavior.rb
class String
# Enables more predictable duck-typing on String-like classes. See <tt>Object#acts_like?</tt>.
def acts_like_string?
true
end
end
拍子抜けするような簡素なメソッドです。コメントによれば、Stringと似た振る舞いをするクラスで、duck-typingがより期待どおりに動作するためのメソッドであるとのことです。
Object#acts_like?
を見てみましょう。devdocs.ioで探すのが早いですね。
object/acts_like.rb
class Object
# A duck-type assistant method. For example, Active Support extends Date
# to define an <tt>acts_like_date?</tt> method, and extends Time to define
# <tt>acts_like_time?</tt>. As a result, we can do <tt>x.acts_like?(:time)</tt> and
# <tt>x.acts_like?(:date)</tt> to do duck-type-safe comparisons, since classes that
# we want to act like Time simply need to define an <tt>acts_like_time?</tt> method.
def acts_like?(duck)
respond_to? :"acts_like_#{duck}?"
end
end
大意: ダックタイピングのアシスタントメソッドです。
例: Active SupportではDateをextendして#acts_like_date?
メソッドを定義し、Timeをextendして#acts_like_time?
を定義しています。これにより、x.acts_like?(:time)
や<tt>x.acts_like?(:date)
などのようなダックタイプ安全かどうかの比較を行えます。Timeとして振る舞って欲しいクラスにはacts_like_time?
メソッドが定義されていて欲しいからです。
String#acts_like_string?
はダックタイピングのためだったんですね。開発者が直接使うというより、Rails内部で使われることが多いのではないかととりあえず推測しました。
String#acts_like_string?
を使ったダックタイピングがRailsの中でどのように使われているのか、ユースケースを掘り出してみましょう。
Railsでの用例
GitHubで検索してみると、activerecord/lib/active_record/sanitization.rbに一箇所だけ用例がありました。
コメントや途中のコードは適宜...
で省略しています。
module ActiveRecord
module Sanitization
extend ActiveSupport::Concern
module ClassMethods
protected
# Accepts an array or string of SQL conditions and sanitizes
# them into a valid SQL fragment for a WHERE clause.
#
# sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4])
# # => "name='foo''bar' and group_id=4"
...
alias :sanitize_sql :sanitize_sql_for_conditions
alias :sanitize_conditions :sanitize_sql
...
def replace_bind_variable(value, c = connection) # :nodoc:
if ActiveRecord::Relation === value
value.to_sql
else
quote_bound_value(value, c)
end
end
...
def quote_bound_value(value, c = connection) # :nodoc:
if value.respond_to?(:map) && !value.acts_like?(:string)
if value.respond_to?(:empty?) && value.empty?
c.quote(nil)
else
value.map { |v| c.quote(v) }.join(",")
end
else
c.quote(value)
end
end
...
end
...
end
end
いつの間にかActive Recordのprotectedなモジュールにまで足を踏み入れてしまいました。
大意:
# SQL条件の配列または文字列を受け取り、サニタイズして
# WHERE句で使える有効なSQLフラグメントにする
#
# sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4])
# # => "name='foo''bar' and group_id=4"
#sanitize_sql_for_conditions
->#sanitize_sql_array
->#replace_bind_variable
->#quote_bound_value
が呼び出され、その中で以下の文があります。
if value.respond_to?(:map) && !value.acts_like?(:string)
文字列でないかどうかの判定に使っているようですが、正直これだけだとありがたみがピンときませんでした。
acts_like?
の使い道
acts_like?
はどんなふうに使うことが想定されているのでしょうか。
Object#acts_like?(ActiveSupport)という記事を見かけたので、以下に引用します。
@tkawa ロガーに共通のメソッドを備えていたらacts_like? :logger、配列っぽかったらacts_like? :array(XMLのNodeSetなど)とは考えたんですが、普通にrespond_to?で判定するのとどっちが楽か分からなくなりました。
— 北市真 (@KitaitiMakoto) 2013年5月22日
@KitaitiMakoto acts_like? :array はありかも、と思ったけどやっぱり is_a? Enumerable のほうがよさそうだし、ダックタイピングにうまくはまりそうな例が難しいですね…。
— Toru KAWAMURA (@tkawa) 2013年5月22日
- 個人的には、
respond_to?
で単にあるメソッドがあるか調べるのに比べて、コードの意図が明確になると思います。
Object#acts_like?(ActiveSupport)より
当初はRails内部での利用が主なのかなと推測しましたが、自分のクラスにacts_like?
を独自に実装して、同じ流儀で判定できるようにすることが想定されているのかもしれないと思いました。
参考
morimorihogeさんのサジェスチョン
acts_like?
の利用法について、BPS Webチーム部長のmorimorihogeの知恵をお借りしましたので自分なりに再構成しました。以下はコードからわかる範囲での見解に基づいていますのでご了承ください。
#acts_like?
については、インターフェースのチェックという意味合いが考えられます。たとえば#respond_to?
だと個別のメソッドの存在確認しかできないので、StringならStringとしてのインターフェース集合があるかどうかをチェックするのによさそうです。
メソッドの存在確認を#respond_to?
で行おうとすると、たとえば#perform
のような具体的でないメソッド名はクラスによって動作が異なってしまう可能性があるので、振る舞いまでは#respond_to?
などでは調べきれません。
ひとつの方法ですが、(Railsではなく)RubyのModule#include?を使ってモジュールの有無をチェックする方がより確実かもしれません。
module A
end
class B
include A
end
class C < B
end
B.include?(A) #=> true
C.include?(A) #=> true
A.include?(A) #=> false
ただし、モジュールという概念に収まりきれないような横断的な機能についてインターフェースをチェックするのであれば、#acts_like?
のような方法が役に立つこともあるかもしれません。
なお、behavior.rbのblameは2008-2009年とだいぶ昔です。当時のプルリクbabbc15@ManfredFixを見ると、先ほどの#quote_bound_value
はもともとactiverecord/lib/active_record/base.rbにあったんですね。
おまけ
ダックタイピング https://t.co/MX1ic9GW6B
— 結城浩 (@hyuki) December 20, 2016
関連記事
[Rails5] Active Support Core ExtensionsのStringクラス(1)String#blank?