Metaprogramming Techniques
前面已经讲述了Ruby的一些基础,后面是在Rails里面常见的元编程的技术。
尽管例子都是用Ruby书写的,这些技术大部分可以是对任何动态语言适用的。实际上,Ruby的元编程的语法是在像Lisp,Smalltalk和Perl里借鉴的。
运行时方法查找
我们经常需要的是创建一个根据一些运行时的数据变化的函数的接口。最突出的例子是Rails里面的ActiveRecord的属性的accessor方法。对ActiveReord的方法调用会在运行时被转化成属性的入口。在类—方法的层面上,ActiveRecord提供了相当好的便利:Person.find_all_by_user_id_and_active(42, true) 被转换成特定的SQL查询,当属性不存在的时候会引发一个标准的NoMethodError异常。
这个魔幻之处在于Ruby的method_missing方法。当一个不存在的方法被对象调用的时候,Ruby在引发标准的NoMethodError异常前首先检查对象类有没有method_missing方法。method_missing第一个参数是被调用的方法的名字;剩下的参数就是被调用的方法的参数。任何传递给那个方法的block会直接传给method_missing。一个万昌的函数签名如下:
def method_missing(method_id, *args, &block)
...
end
这里有使用method_missing的缺点:
它比常规的方法查找速度慢。一些简单的测试说明了带有method_missing方法的分派至少要比常会的分派在时间上多花费2到3倍。
因为被调用的方法实际上不存在——这种方法仅仅是在方法查找的最后一步解析的——因此不能向常规的方法一样被文档化和自省。
因为所有的动态的方法必须要通过method_missing方法,如果有很多的代码需要添加不同的方面动态的方法,method_missing方法的函数体会变的非常大。
使用method_missing限制了以后API的兼容性。一旦你依靠method_missing去处理没有方法定义的一些有趣的事情,在以后的API版本当加入新方法可能会破坏用户的预料。
在ActiveRecord里面用的generate_read_methods功能是一一个比较好的另一种方式。ActiveRecord不是等待method_missing去解析调用,而是生成一些属性setter和reader的实现,这些实现可以像常规的方法分派一样调用。
总体上来说,这是一个强大的方法,Ruby这种动态性让Ruby编写方法当方法第一次调用的时候用优化的版本替换自己的方法成为可能。Rails的routing需要快速响应,就用到了这个功能;这个在这一节的后面有描述。
Generative Programming: Writing Code On-the-Fly
一个对于其他的强大的技术是自产生编程——代码写代码
这个技术简单的说,就是写一个shell脚本去自动化一些单调编程。比如,你可能需要产生对每个用户测试装置。
brad_project:
id: 1
owner_id: 1
billing_status_id: 12
john_project:
id: 2
owner_id: 2
billing_status_id: 4
...
如果有一种语言没有可脚本化的测试装置,你必须手工来做。如果数据增加,这会变的杂乱,而且当有些很奇怪的原数据依赖的时候,这几乎不太可能手工做。单一的自产生编程可以让你在原数据里产生测试夹具。尽管不是很理想,这会在很大程度上改进手工书写。但是比较头疼的是维护:你必须把这个脚本合并在你的创建的过程里来保证这个夹具会在源数据变化时重新产生。
这个很少见,但在ROR里是需要的。几乎ROR所有的应用配置是可脚本化的,很大部分用了内部的DSL。在内部的DSL,在你可以使用Ruby全部的能力,不仅仅是库作者给你指定的特定接口。
回到先前的例子,ERb能让这个工作变的很容易。可在上面YAML文件里加入Ruby的代码(这些代码用ERb<% %>和<%= %>标记)和任何我们需要的逻辑:
<% User.find_all_by_active(true).each_with_index do |user, i| %>
<%= user.login %>_project:
id: <%= i %>
owner_id: <%= user.id %>
billing_status_id: <%= user.billing_status.id %>
<% end %>
对于这个小技巧,ActiveRecord's再简单不过了:
yaml = YAML::load(erb_render(yaml_string))
using the helper method erb_render:
def erb_render(fixture_content)
ERB.new(fixture_content).result
end
自产生变成常用 Module#define_method 或者 class_eval 和def区创建运行中的方法。ActiveRecord 对于属性的 accessors使用了这些技巧;generate_read_method噢共能定义了setter and reader方法,这些方法是ActiveRecord class的实例方法,能够减少使用times method_missing (a relatively expensive technique) 需要次数。