Rubyにはメソッド探索の最後に呼ばれる(つまり、メソッドが見つからないときに呼ばれる)フック的なメソッドとして method_missing というメソッドがあります。これを上手に利用することで黒魔術的なコードが書けてかっこいいですw

#!/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 メソッドみたいに何が来るかわからないようなときには非常に便利そうですね。

このエントリーをはてなブックマークに追加