Ruby语言的动态性使得想仅仅通过查找相关API文档来学习Rails是完全不够的,因为API文档只记录了静态代码定义的方法,而Rails中有很多的方法都是在加载时或运行时动态生成的,想要了解这些方法必须要阅读源代码。本文以Rails工程下的config/application.rb文件中的一个方法调用为例,分析方法调用的具体过程。
1. 问题描述
假设Rails工程叫做Sample,那么在config/application.rb中会定义Sample::Application,并可以在其中调用config方法,如下:
1 module Sample
2 class Application < Rails::Application
3 ...
4 config.autoload_paths << "#{config.root}/lib/validators"
5 ...
6 end
7 end
config方法在API文档中是不存在的,下面将分析config方法是如何被找到的。
2. 方法查找
在Sample::Application的定义中,self=Sample::Application,所以config是Sample::Application的类方法。查找Sample::Application的祖先链,并逐一查找,终于在Rails::Railtie中找到了该方法,如下:
1 Sample::Application.ancestors
2 # => [FirstApp::Application, Rails::Application, Rails::Engine, Rails::Railtie, Rails::Initializable, Object, PP::ObjectMixin, ActiveSupport::Dependencies::Loadable, V8::Conversion::Object, JSON::Ext::Generator::GeneratorMethods::Object, Kernel, BasicObject]
3
4 Rails::Railtie.singleton_methods(false).grep /^config$/
5 # => [:config]
6
7 #也可以用source_location来查看方法的位置
8 Sample::Application.method(:config).source_location
9 # => ["/usr/local/rvm/gems/ruby-2.1.1/gems/railties-4.1.4/lib/rails/railtie.rb", 123]
最终在Rails::Railtie中找到该方法定义,如下:
1 module Rails
2 class Railtie
3 ...
4 class << self
5 ...
6 delegate :config, to: :instance
7 ...
8 end
9 ...
10 end
11 end
可以看到第6行采用了元编程的方式定义了config方法,则直到文件被加载完毕config方法才会生成,因此该方法不会出现在API文档中。
3. 方法的定义
config方法的定义是挺有趣的,它利用了Rails对Module的扩展delegate方法进行了定义,采用了委托模式,如下是delegate方法中的核心部分:
1 class Module
2 ...
3 def delegate(*methods)
4 ...
5 methods.each do |method|
6 ...
7 method_def = [
8 "def #{method_prefix}#{method}(#{definition})",
9 " _ = #{to}",
10 " if !_.nil? || nil.respond_to?(:#{method})",
11 " _.#{method}(#{definition})",
12 " else",
13 " #{exception unless allow_nil}",
14 " end",
15 "end"
16 ].join ‘;‘
17
18 module_eval(method_def, file, line)
19 end
20 end
21 end
根据上面代码,delegate :config, to: :instance 相当于如下代码(进行了省略),即在Rails::Railtie中定义了一个类方法:
1 class << Rails::Railtie
2 def config
3 instance.config
4 end
5 end
4. 方法的调用
最后在问题描述中,Sample::Application调用了了方法,调用过程相当于执行了如下代码:
1 Sample::Application.instance.config
instance方法是类方法,定义于Rails::Railtie中,其定义以及根据instance代码得到的中间结果如下:
1 #instance的定义
2 class << self
3 def instance
4 @instance ||= new
5 end
6 end
7
8 #根据instance的定义,得到中间结果
9 Sample::Application.new.config
此时self是Sample::Application.new,config方法是一个实例方法,定义于Rails::Application中,其定义以及最终执行结果如下:
1 #config方法的定义
2 def config #:nodoc:
3 @config ||= Application::Configuration.new(find_root_with_flag("config.ru", Dir.pwd))
4 end
5
6 #最终执行结果
7 Application::Configuration.new(find_root_with_flag("config.ru", Dir.pwd))
由此可以知道config方法是如何被调用的了。
5. 总结
上面分析了Rails::Application中的一个方法调用的例子,可以看出还是比较复杂的,尤其是在对self的判断上要绝对清晰,否则可能会对被调用的方法产生判断错误。Module#delegate方法是一个Rails中常用的委托方法,未来将详细讨论下该方法。