method_missing经常用来写Ruby的元编程。例如,如果你的UserModel有个email的属性,你就可以通过
User.find_by_email('[email protected]')
来查找,这是如果User并没有定义这个方法,那么ActiveRecord::Base就会处理这样的请求。后面我们会具体分析这过程的逻辑和实现,并学习处理。
respond_to?在实现动态编程中也经常会用到,通常我们在使用respond之前判断是否有respond。
那么我们具体看看实现:
假设我们有Legislator类,我们将实现从find_by_first_name('John') 到find(:first_name => 'John')的动态逻辑
如下:
class Legislator
# Pretend this is a real implementation
def find(conditions = {})
end
# Define on self, since it's a class method
def self.method_missing(method_sym, *arguments, &block)
# the first argument is a Symbol, so you need to_s it if you want to pattern match
if method_sym.to_s =~ /^find_by_(.*)$/
find($1.to_sym => arguments.first)
else
super
end
end
end
按照道理来说,这是通过样的逻辑,只是Legislator.respond_to?(:find_by_first_name)会返回false那么我们需要respond_to?来支持。
class Legislator
# ommitted
# It's important to know Object defines respond_to to take two parameters: the method to check, and whether to include private methods
# http://www.ruby-doc.org/core/classes/Object.html#M000333
def self.respond_to?(method_sym, include_private = false)
if method_sym.to_s =~ /^find_by_(.*)$/
true
else
super
end
end
end
那么现在有一个问题,就是我们的重复了。这就很不DRY。所以,我们可以和ActiveRecord的学习,看看是怎么处理重复的问题的。实际上ActiveRecord把逻辑封装在了ActiveRecord::DynamicFinderMatch以便不会在method_missing 和respond_to?重复代码
class LegislatorDynamicFinderMatch
attr_accessor :attribute
def initialize(method_sym)
if method_sym.to_s =~ /^find_by_(.*)$/
@attribute = $1.to_sym
end
end
def match?
@attribute != nil
end
end
class Legislator
def self.method_missing(method_sym, *arguments, &block)
match = LegislatorDynamicFinderMatch.new(method_sym)
if match.match?
find(match.attribute => arguments.first)
else
super
end
end
def self.respond_to?(method_sym, include_private = false)
if LegislatorDynamicFinderMatch.new(method_sym).match?
true
else
super
end
end
end
method_missing的缓冲
显然method missing效率不好,那么太多的method missing一定导致很慢。所以另外一个我们可以学习ActiveRecord的地方是我们可以在定义method missing的同时发送到正定义的方法,如下:
class Legislator
def self.method_missing(method_sym, *arguments, &block)
match = LegislatorDynamicFinderMatch.new(method_sym)
if match.match?
define_dynamic_finder(method_sym, match.attribute)
send(method_sym, arguments.first)
else
super
end
end
protected
def self.define_dynamic_finder(finder, attribute)
class_eval <<-RUBY
def self.#{finder}(#{attribute}) # def self.find_by_first_name(first_name)
find(:#{attribute} => #{attribute}) # find(:first_name => first_name)
end # end
RUBY
end
end
测试
创建LegislatorDynamicFinderMatch来测试逻辑,下面是RSpec的例子:
describe LegislatorDynamicFinderMatch do
describe 'find_by_first_name' do
before do
@match = LegislatorDynamicFinderMatch.new(:find_by_first_name)
end
it 'should have attribute :first_name' do
@match.attribute.should == :first_name
end
it 'should be a match' do
@match.should be_a_match
end
end
describe 'zomg' do
before do
@match = LegislatorDynamicFinderMatch(:zomg)
end
it 'should have nil attribute' do
@match.attribute.should be_nil
end
it 'should not be a match' do
@match.should_not be_a_match
end
end
end
当然,如果你的动态实习需要一些输入的话,很难免你需要用到RSpec,如下:
describe Legislator, 'dynamic find_by_first_name' do
it 'should call find(:first_name => first_name)' do
Legislator.should_receive(:find).with(:first_name => 'John')
Legislator.find_by_first_name('John')
end
end
总之,如果你正在写动态方法你应该考虑respond_to?