Rubyにはメソッド探索の最後に呼ばれる(つまり、メソッドが見つからないときに呼ばれる)フック的なメソッドとして method_missing というメソッドがあります。これを上手に利用することで黒魔術的なコードが書けてかっこいいですw
method_missing には、第一引数に呼ばれたメソッド名、第二引数にその(見つからなかった)メソッドに対して与えた引数が渡されます。
適当にサンプルをでっち上げてみました。指定するメソッド名を xxx_xxx みたいな形にして、アンダーバーの前がモジュール名、後が実際のメソッド名です。いまいち使い道が見えてこないですが、ActiveRecord とかで実際にごにょごにょと使われてるようなのでその辺を参考にすると良いかもしれません。find_by_xxx メソッドには多分使われてるはず。
と思って調べてみたらありました。こんな感じです。長い。。。
とりあえず、同じようなメソッドを何度も書かなくていい、っていうのがメリットなのかな。find_by_xxx メソッドみたいに何が来るかわからないようなときには非常に便利そうですね。
#!/usr/bin/ruby module Sasata299 # 適当なモジュールを定義 def hoge(num) return num * 2 end def fuga(num) return num + '299' end end def method_missing(action, *args) if action.to_s =~ /(.+)_(.+)/ eval "include #{$1.capitalize}" __send__ $2, *args end end p sasata299_hoge(10) # 20 p sasata299_fuga('sasata') # sasata299
method_missing には、第一引数に呼ばれたメソッド名、第二引数にその(見つからなかった)メソッドに対して与えた引数が渡されます。
適当にサンプルをでっち上げてみました。指定するメソッド名を xxx_xxx みたいな形にして、アンダーバーの前がモジュール名、後が実際のメソッド名です。いまいち使い道が見えてこないですが、ActiveRecord とかで実際にごにょごにょと使われてるようなのでその辺を参考にすると良いかもしれません。find_by_xxx メソッドには多分使われてるはず。
と思って調べてみたらありました。こんな感じです。長い。。。
def method_missing(method_id, *arguments) if match = /^find_(all_by|by)_([_a-zA-Z]\w*)$/.match(method_id.to_s) finder = determine_finder(match) attribute_names = extract_attribute_names_from_match(match) super unless all_attributes_exists?(attribute_names) self.class_eval %{ def self.#{method_id}(*args) options = args.last.is_a?(Hash) ? args.pop : {} attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args) finder_options = { :conditions => attributes } validate_find_options(options) set_readonly_option!(options) if options[:conditions] with_scope(:find => finder_options) do ActiveSupport::Deprecation.silence { send(:#{finder}, options) } end else ActiveSupport::Deprecation.silence { send(:#{finder}, options.merge(finder_options)) } end end }, __FILE__, __LINE__ send(method_id, *arguments) elsif match = /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/.match(method_id.to_s) instantiator = determine_instantiator(match) attribute_names = extract_attribute_names_from_match(match) super unless all_attributes_exists?(attribute_names) self.class_eval %{ def self.#{method_id}(*args) if args[0].is_a?(Hash) attributes = args[0].with_indifferent_access find_attributes = attributes.slice(*[:#{attribute_names.join(',:')}]) else find_attributes = attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args) end options = { :conditions => find_attributes } set_readonly_option!(options) record = find_initial(options) if record.nil? record = self.new { |r| r.send(:attributes=, attributes, false) } #{'record.save' if instantiator == :create} record else record end end }, __FILE__, __LINE__ send(method_id, *arguments) else super end end def determine_finder(match) match.captures.first == 'all_by' ? :find_every : :find_initial end def extract_attribute_names_from_match(match) match.captures.last.split('_and_') end
とりあえず、同じようなメソッドを何度も書かなくていい、っていうのがメリットなのかな。find_by_xxx メソッドみたいに何が来るかわからないようなときには非常に便利そうですね。
Smalltalkでの用途は大体委譲用です。他に面白いものとしてメッセージの保存があります。
| message |
message := ( MessageCapture new ) arg0: 1 arg1: 2 arg2: 3. "わかりやすくカッコを付けてますが本来不要です"
message sendTo: TypeA. "class objectであるTypeAにarg0〜3.メッセージを送信"
これの機能はスレッドに応用できます。
| future reciever capture |
future := ProcessVariable new.
reciever := FileStream stdout.
[
[ future value sendTo: reciever. ] repeat.
] fork.
capture := MessageCapture new.
"並列実行しながら一文字づつ標準入力に文字を表示"
future
value: ( capture nextPut: $H );
value: ( capture nextPut: $e );
value: ( capture nextPut: $l );
value: ( capture nextPut: $l );
value: ( capture nextPut: $o ).
SmalltalkにはdoesNotUnderstandを使った処理が色々と有るんでSqueakとか参考にしてみると面白いかと思います。