ついカッとなって書いた。たのしかった。
https://github.com/tagomoris/dyna_mo
どういうことかというと、つまりこういうことだ!
require 'dyna_mo' module MyModule class MyClass attr_accessor :num def initialize; @num = 0; end def name; "name"; end def sum(numbers); @num + numbers.reduce(:+); end end end
こういう割と普通なクラスに対して
dynamo_define('MyModule::MyClass', :mytest_case_default) do def_method(:initialize) do @num = 1 end def_method(:name) do # #name を差し替える "dummyname" end def_instance_method(:name, :mytest_case1) do # #name 差し替えの別パターン "dummyname1" end def_method(:sum) do |numbers| @num + numbers.reduce(:+) + 1 end def_class_method(:create) do |init_num=0| # 元の定義になかった MyClass.create の追加 obj = self.new obj.num = init_num obj end end
特定のとき*1にこのメソッドをこういうふうに上書きしてね!*2 ということを指定して
class SynopsisTest < Test::Unit::TestCase def test_synopsis assert { MyModule::MyClass.new.name == "name" } # 元の状態 obj = MyModule::MyClass.new assert { obj.num == 0 } # インスタンス変数 @num は 0 assert { obj.name == "name" } dynamo_context(:mytest_case_default) do # ここから定義をスイッチ assert { obj.num == 0 } # #initialize is not overridden # これは @num の参照だけで変更なし assert { MyModule::MyClass.new.name == "dummyname" } # ウヒョー! assert { obj.name == "dummyname" } # 生成済みのオブジェクトのメソッドだってもちろん変わる! assert { MyModule::MyClass.new.num == 1 } # initializeだって上書きできる! assert { MyModule::MyClass.new.sum([1,2,3]) == (1+(1+2+3)+1) } # 普通に引数だって与えられる! assert { MyModule::MyClass.create(100).num == 100 } # ちょっとこのテスト専用に便利メソッド! end end
こういう感じで使う! dynamo_context(:mytest_case_default) に与えているブロックの中でだけ MyClass#name や MyClass#sum の動きが違ったり MyClass.create が使えたりする!!!!!!!!!!!!!!!!!!!
コンテキスト外で作った obj のインスタンス変数 @num は変化していないがそれを使うメソッドだけが書き変わっている! ウヒー!
もちろん別のコンテキスト名を指定すれば別の動きかたをする!
class SynopsisTest < Test::Unit::TestCase def test_synopsis dynamo_context(:mytest_case1) do # もちろん違うコンテキストでは違う動きになる! assert { obj.name == "dummyname1" } end dynamo_define(MyModule::MyClass, :onetime_context) do def_method(:name) do "onetime" end end dynamo_context(:onetime_context) do # 違う! 動きに!!! assert { obj.name == "onetime" } end end end
もちろんだがダイナミックスコープなので、直接記述されていなくても、与えられたブロックからの呼び出し階層のどこかに存在するメソッドであれば容赦なく書き変わっている。例えば次のコードは MyClass を継承して MyClass2#name を呼んでいるが、親クラスの宣言は :onetime_context 内では書き変わっているので自動的に MyClass2#name の結果も変わる! カッコイイ!!!!!!
module MyModule; class MyClass2 < MyClass; end; end class Synopsis2Test < Test::Unit::TestCase def test_onece_more dynamo_define(MyModule::MyClass, :onetime_context) do def_method(:name) do "onetime" end end dynamo_context(:onetime_context) do assert { MyModule::MyClass2.new.name == "onetime" } # 継承元の書き換えがここにも影響! end end end
このような感じで自由の地平が開けます。ぜひどうぞ。
FAQ
なんで作ったの?
RubyKaigiの質問コーナーでdynamic scope欲しい!って言ったらMatzに「dynamic scopeを使わないためにRubyを作っている」と答えられたのでムシャクシャしてやった。
なんで作ったの?
power_assert とかちょっと真面目に使ってみたかった。が、まだちょっとバグいね*3。
なんで作ったの?
RubyHirobaでうまい方法ないかなーと思っていたら @_ko1 さんにスレッドローカル変数とModule#prependを使うという方法を授かったのでついやってしまった。責任の半分は @_ko1 さんにあると思う。
本番で使っていい?
絶対にやめましょう。ありとあらゆる不幸が振りかかります。
本番で使っちゃおうかな?
万が一にでも自分が数年後とかに転職した先で見付けた本番コードで使われてたりしたら、たとえ退職者であろうとも書いた人のリアル住所を割って襲撃に行くかもしれません。やめておいた方が賢明です。
ぶっちゃけこれ必要なのって設計が悪いんじゃない?
はい。
例えばマルチスレッドをゴリゴリ使いまくっているのに超大事なところにシングルトンオブジェクトが居座っているFluentdというソフトウェアなんかがあって、そいつがマジでもうどうにもならなかったりするので作りました。しかしFluentd v0.12 ではそのシングルトンオブジェクトが排除されたため不要になりました。おおお……。。。
まあ、しかし、外部gemの動作に依存したコードのテストを書かなければならない時ってあるじゃないですか。ナントカAPIとかナントカSDKみたいなgemを叩きたいんだけどそいつがバックエンドをモックに置き換えられなくなってるとか! あるでしょ!
たのしかった?
はい!!!!!!!!!!!!!! 特異クラスに対してprependとかしてて脳から変な汁が出そうになった!!!