深読みしないRuby Refinements
ruby 2.1からの Refinements を使ってみようとして、思い通り通りに動かないことが多かったので、まとめた資料です。 仕様をまとめたものはRefinementsSpecにあるのですが、そこまで読まずに簡単に試してみたい場合にポイントとなるところをまとめます。
基本的な使い方
例として文字列のサイズを偽るRefinementsを使います。
これ以降の例でもusing A
が出てきますが、同じモジュールです。
Stringの長さが-1になっていれば、このRefinementsの影響を受けています。
module A refine String do def size -1 end end end using A 'hoge'.size # => -1
using
を使うと、それ以降のスコープでリファインしたメソッドの挙動が変わります。
クラスやモジュールでスコープを切ることもできます。便利ですね。
class B using A def hoge 'hoge'.size end end B.new.hoge # => -1 'hoge'.size # => 4
スコープについて
using
を使うとそれ以降の処理に影響します。
以下の例では、using
を使う前後で結果が変わっています。
'hoge'.size # => 4 using A 'hoge'.size # => -1
using
以降のクラス定義も影響を受けます。
以下の例ではClass B
内ではusing
を仕様していないにもかかわらず影響を受けます。
場合によっては、あまり想定しない影響を起こすかもしれません。
using A class B def hoge 'hoge'.size end end B.new.hoge # => -1 'hoge'.size # => -1
モジュール内のクラス全てにリファインメントを影響させることもできます。 こちらは通常意図する使い方になると思います。
module D using A class B; end class C def fuga 'fuga'.size end end end D::C.new.fuga # => -1
スコープについて2
クラス定義内でも順序を入れ替えるとusing
前のメソッド定義は影響を受けません。
この例ではメソッド定義後にusing
を使っているため、上書きされません。
# Bad example class B def hoge 'hoge'.size end using A end B.new.hoge # => 4
ややこしくなるので、通常は先頭でusing
を呼ぶ方が良さそうです。
以下の例では、後からRefinementsの影響を入れるために、クラスを再オープンしてusing
を使用しています。
しかし、元のクラスで定義されたメソッドはRefinementsの影響を受けませんので、この使い方は無意味です。
# This does NOT work class B def hoge 'hoge'.size end end class B using A end B.new.hoge # => 4
直接using
した範囲でしか影響しない
ここははまりどころだと思いますが、 Refinements の影響を受けるのは直接using
を使用されたクラスのみです。
そのクラスから呼ばれていても、別のクラスのメソッドを経由した場合は、元のメソッドが呼ばれます。
ややこしいのでサンプルで説明します。
class B def hoge 'hoge'.size end end class C using A def fuga B.new.hoge end end C.new.fuga # => 4
class C
でusing
を使用しています。しかし実際にString#size
を呼ぶのはclass B
です。
そのためclass C
でのusing
の影響は受けず、元の長さを返します。
class C
でのusing A
で文字列の長さを変えたいと意図していますが、そのようには動作しません。
さらに別の例があります。こちらもはまりやすいパターンです。
class String def my_size size end end class B using A def hoge 'hoge'.my_size end end B.new.hoge # => 4
Stringに別のメソッドmy_size
を定義して、その中でsize
を呼んでいます。
my_size
はusing
したB
から呼ばれるので、一見上書きしたsize
が呼ばれそうですが、String#my_size
には影響していないので、呼ばれるのは元のsize
です。
まとめ
Refinementsはあくまでそのクラスから見たメソッドの見え方を変えるもので、他のクラスには一切影響しません。たとえメソッド呼び出しの契機となるクラスでusing
を使っていても、他のクラスを経由した場合の動作は変わりません。*1
モンキーパッチとして使う場合、表側に近いクラスの挙動は変更しやすいですが、奥の方にあるクラス(内部向けのクラスなど)や多数のクラスから呼ばれるメソッドの挙動を変えるには工夫が必要そうです。
「限定されたスコープで動作する」というところがRefinmentsのポイントなので、スコープが不足していると思ったら、別の方法も検討しましょう。
*1:わかりやすくクラスと書いてますが、もちろん使い方によってはクラスだけではないです