Rubyで他のクラスを拡張したい場合
いろんな言語で既存のクラスを機能拡張することはあると思いますが、Rubyでももちろん存在します。
ですが、Rubyの場合比較的自由度が高いのでいろんな方法で拡張できます。
昔からある方法を使うのもいいですが、Ruby2以降で入った機能なども考慮するとなかなかパターンが合ったので自分用に整理したかったのと、きっと他の方法もあるんだろうとおもってそういうのがあれば是非教えてもらいたくて記事を書こうと思いました。
そもそもいい、わるいは別として同じ結果を取得する方式でもいろいろあって面白いのでそういう意味でも他の方の意見も聞きたいなぁと。
あと、それぞれの方法のメリット・デメリットなど一般的にいわれているものがあればそれも教えてほしいです。
さて、それでは今回拡張するクラスです。
拡張対象のクラス
class C1
def m(act, *arg)
case act
when :sym1
p [:sym1, arg]
when :sym2
p [:sym2, arg]
else
raise StandardError.new("unknown action : #{act}")
end
end
end
m
というメソッドが「第一引数act
の値によって、いろいろ異なる振る舞いをする」として与えられていて、それを使う側がactのパターンを増やしたいという場合です。
だいぶ恣意的な感じですが、実は最近触っていたselenium
のwebdriver
クラスのfind_element
がこんな引数になってるんですね(実装はもちろん違いますが)
find_element(:xpath, "xpathの式")
, find_element(:css, "cssのセレクタ式")
みたいな感じでどんな方式でエレメントを取得するか第一引数で指定して、その実際のパスを第二引数で渡す、というような処理です。
さて、ここで、act
に:sym3
を渡して何か処理したい場合、クラスC1
を拡張したいですね。
特に今回の元ネタの処理、webdriver
でいうと、実際はact
がsym1
,sym2
で事足りるんだけど、実際に使う上でのショートカットとしてsym1
やsym2
を使って別のsym3
を使えるようにしたいというケースです。
まったくC1クラスとは独立したユーティリティクラスを作る
単純ですね。どこのクラスのメソッドとかは関係なく、C1
クラスのオブジェクトを受け取って目的の処理をする処理を作ればいいわけです。
def m_ext(c1_obj)
c1_obj.find_element(:sym1, :hogehoge_arg)
end
ただし、この方法にしてしまうと、sym3
の場合のみ別のクラスのメソッドになってしまうので元のC1
クラスのメソッドと合わせて使うとなんか見た目がおかしくなります。
c1 = C1.new
c1.m(:sym1, :hoge)
c1.m(:sym2, :piyo)
m_ext(c1) # <= これだけ書き方が違う
特異メソッドを定義する
上のが嫌な場合(嫌じゃなければそれでもいいんですが)、次に簡単なのは、作ったc1
オブジェクトに対して特異メソッドを定義してしまうことでしょうか。
c1 = C1.new
def c1.m(act, arg)
case act
when :sym3
p [:sym3, arg]
else
super
end
end
c1.m(:sym1, :arg1)
c1.m(:sym3, :arg1)
特異メソッドを直接定義するんではなく、特異クラスに専用のモジュールを取り込む
特異メソッドを定義してもいいんですが、じゃぁ、この他にもいろいろ拡張したいメソッドが出てきたらそれごとにdef c1.xxx
を追加していくことになりますね。それでいいことも多いですが、なんかまとまりが無いですね。拡張した部分だけをまとめて定義してc1
に注入できるといい感じです。
module C1Ex
def m(act, arg)
case act
when :sym3
p [:sym3, arg]
else
super
end
end
end
c1 = C1.new
c1.extend(C1Ex)
c1.m(:sym1, :arg1)
c1.m(:sym3, :arg1)
拡張したいところがmodule
にまとまったので個人的にはさっきまでのより綺麗な気がします。
C1クラスのサブクラスを作る
c1
オブジェクトだけでなく、C1
クラスから作られたすべてのオブジェクトでそういう振る舞いにしたい場合(というか多くの場合そうでしょう)C1自体を拡張したいですね。というか普通のオブジェクト指向言語ならこの方式ですかね。
class MyC1 < C1
def m(act, arg)
case act
when :sym3
p [:sym3, arg]
else
super
end
end
end
MyC1.new.m(:sym1, :arg1)
MyC1.new.m(:sym3, :arg1)
C1クラス自体を変更する
他の多くの言語と異なりRubyはオープンクラスという仕組みがあり、既存のクラスを再度定義(追加定義?)することができます。
その方法を使えば、拡張というよりもそもそも元のクラスを書き換えられます。いわゆるモンキーパッチというやつですね。
class C1
def m(act, arg)
case act
when :sym1
p [:sym1, arg]
when :sym2
p [:sym2, arg]
when :sym3
p [:sym3, arg]
else
raise StandardError.new("unknown action : #{act}")
end
end
end
c1 = C1.new
c1.m(:sym1, :arg1)
c1.m(:sym3, :arg1)
これやれば何でもありですね。ですが、もともとのC1#m
メソッドが複雑だったりするとなかなか大変ですし、C1
を使う他のクラスがいたりするとそこに影響もいろいろ出てきます。
###c1クラス自体を変更するが、モジュールで既存メソッドを拡張する
既存のクラスに無いメソッドを既存のモジュールを使って拡張するのは昔からあるinclude
でよかったんですが、
既存のクラスに有るメソッド自体を拡張するのはいろいろトリッキーなことをする必要がありました(rails
のalias_method_chain
とかですね)
そういったことをするために、prepend
という処理が加えられました。これを使えば、モジュールでをprepend
することで既存のメソッドを変更することができます。
module C1Ex
def m(act, arg)
case act
when :sym3
p [:sym3, arg]
else
super
end
end
end
class C1
prepend C1Ex
end
C1.new.m(:sym1, :arg1)
C1.new.m(:sym3, :arg1)
ちなみに、これをprepend C1Ex
でなく、昔ながらのinclude C1Ex
にすると、m(:sym3, :arg1)
は実行できるのですが、m(:sym1, :arg1)
が動かなくなります。理由は、include
の場合C1Ex
が継承階層でいうと、C1より親に来るからです.
というかそうならないように継承階層に組み込もう!というのがprepend
ということですね。
###c1クラス自体を変更するが、影響範囲はrefine
で抑える
モンキーパッチでもprepend
でもC1
クラスを使うすべての箇所に影響しますが、refine
を使えばその箇所を限定できます。
module C1Ex
refine C1 do
def m(act, arg)
case act
when :sym3
p [:sym3, arg]
else
super
end
end
end
end
using C1Ex
C1.new.m(:sym1, :arg1)
C1.new.m(:sym3, :arg1)
refine
は比較的最近の機能で2.0
ではトップレベルにしかかけませんでした。2.1
以降はclass
定義の中でも使えます。ご注意くださいませ。
class UserClass
using C1Ex #あるクラスの中だけでC1クラスを拡張する
def initialize
C1.new.m(:sym1, :arg1)
C1.new.m(:sym3, :arg1)
end
end
UserClass.new
一律既存クラスの動きを変えたいのではなく、特定のクラス内だけ動きを変えたい場合はruby 2.1
以降はこの方法が一番いいんじゃないかと思っています。
ちなみによく分かってないのですが、refine
内で定義したメソッドたちの検索順序はこれを見る限り、prepend
のようにC1よりも先にくるんですかね??
### まとめ
いやぁ、いろいろありますね。
2016/4/27 追記
コメントでrefinement
とprepend
などを使った時のメソッドの検索順序の資料を教えてもらいました。こんなの有るんですね。
When looking up a method for an instance of class C Ruby checks:
-
If refinements are active for C, in the reverse order they were activated:
-
The prepended modules from the refinement for C
-
The refinement for C
-
The included modules from the refinement for C
-
-
The prepended modules of C
-
C
-
The included modules of C
-
If no method was found at any point this repeats with the superclass of C.
だそうです。こんなの日常使うプログラムで全部同時につかったら混乱してしまいそうですが、試してみます。
class B
def m
p :B_m
end
end
module PrependedModuleOfC
def m
p :PrependedModuleOfC_m
super
end
end
module IncludedModuleOfC
def m
p :IncludedModuleOfC
super
end
end
class C < B
prepend PrependedModuleOfC
include IncludedModuleOfC
def m
p :C_m
super
end
end
module IncludedModuleFromRefinementForC
def m
p :IncludedModuleFromRefinementForC_m
super
end
end
module PrependeModuleFromRefinementForC
def m
p :PrependeModuleFromRefinementForC_m
super
end
end
module RefinementForC
refine C do
include IncludedModuleFromRefinementForC
prepend PrependeModuleFromRefinementForC
def m
p :RefinementForC_m
super
end
end
end
using RefinementForC
p C.ancestors
C.new.m
実行結果
$ ruby -v search_order.rb
ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-darwin15]
[PrependedModuleOfC, C, IncludedModuleOfC, B, Object, Kernel, BasicObject]
:PrependeModuleFromRefinementForC_m
:RefinementForC_m
:IncludedModuleFromRefinementForC_m
:PrependedModuleOfC_m
:C_m
:IncludedModuleOfC
:B_m
おぉ、資料のとおりに呼ばれてますね。
refine
の中でinclude
やprepend
が使えるのは知りませんでした。
refine
の中の分はacestors
には現れないんですね。
確かにこの動きを見ると
ちなみによく分かってないのですが、
refine
内で定義したメソッドたちの検索順序はこれを見る限り、prepend
のようにC1よりも先にくるんですかね??
と疑問に思っていたところは:RefinementForC_m
が:C_m
より先に表示されているので、そうなっているようですね。