前言
ActiveSupport 是 Ruby on Rails 的主要模块之一。它通过 hack 一些 Ruby 的内建类来提供一些通用方法,它还自定义了一些有用的通用功能模块,如 memoize,callback等。研究 ActiveSupport,是因为它在 Rails 的诸多模块中相对底层,依赖较少,而且元编程技巧处处可见。作为第一篇文章,我这次选择分析 ActiveSupport的 aliasing 。因为 aliasing 的源码很简单,它甚至没有被封装到一个模块中,而是直接hack到module类中去了,而且下一篇的memoizable模块会用到其中的一个方法 alias_method_chain 。
大概因为 aliasing 没有遵从 Rails 对模块的命名法(即 ModuleA::moduleB::ClassC 必定放在 module_a/module_b/class_c.rb 文件下),所以加载了 ActiveSupport 模块后,aliasing 不会默认加载,必须手动指定 require 文件路径,下面的代码中可以看出来。
aliasing只提供了两个方法,alias_method_chain 和 alias_attribute。这里只讲 alias_method_chain。后面一个各位有兴趣可以自己看看源码,才5行代码,非常易懂:)
用法
alias_method_chain 对大多数Rubyist来说应该是非常熟悉的。它主要是用来覆盖一个类或模块中已有的方法。当我们在子类中覆盖父类或者被include的模块中的方法时,我们可以像其他语言中一样在子类中定义一个方法,然后使用 super 来调用被覆盖的原方法。但有时候如果我们想覆盖同一个类中定义的方法时,super 就不好用了。这时我们可以用 alias_method 来给方法取个别名,然后再用别的方法来覆盖,例如:
class User
def name
"David"
end
end
u = User.new
puts u.name # => David
User.class_eval do
alias_method :old_name, :name
def name
"Alex"
end
end
puts u.name # => Alex
puts u.old_name # => David
这样虽然可行,但自由度也带来了一定的风险。alias_method可以把方法任意命名,没有一定的规范,很难看出来哪个方法是被覆盖过的。alias_method_chain 则提供了一种覆盖方法的代码规范,它用原方法 + with/without + 后缀名的方式来区分新旧方法。
require 'rubygems'
require 'active_support/core_ext/module/aliasing'
class User
def name
"David"
end
def age
20
end
end
u = User.new
puts u.name
User.class_eval do
def name_with_age
"#{name_without_age}, #{age}"
end
alias_method_chain :name, :age
end
puts u.name
这里 alias_method_chain 把 name 方法改名成 name_without_age,然后把 name_with_age 改名成 name。
以后其他人还可以用 alias_method_chain 继续对 name 方法进行覆盖,不断往 name 方法中加入新东西,就像一条链子一样,alias_method_chain 由此得名。
源码分析
现在看看源码:
class Module
def alias_method_chain(target, feature)
aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
yield(aliased_target, punctuation) if block_given?
with_method, without_method = "#{aliased_target}_with_#{feature}#{punctuation}", "#{aliased_target}_without_#{feature}#{punctuation}"
alias_method without_method, target
alias_method target, with_method
case
when public_method_defined?(without_method)
public target
when protected_method_defined?(without_method)
protected target
when private_method_defined?(without_method)
private target
end
end
def alias_attribute(new_name, old_name)
module_eval <<-STR, __FILE__, __LINE__ + 1
def #{new_name}; self.#{old_name}; end # def subject; self.title; end
def #{new_name}?; self.#{old_name}?; end # def subject?; self.title?; end
def #{new_name}=(v); self.#{old_name} = v; end # def subject=(v); self.title = v; end
STR
end
end
首先,这个文件直接打开了 Ruby 内建的 module 类,写入实例方法 alias_method_chain 。因为所有的 Ruby 类都是 Class 类的实例,而 module 是 Class 类的 superclass,所以 alias_method_chain 可以作为大部分 Ruby 类(和所有自定义类)的类方法。
因为代码很简单,而且一些原编程方法都很直观,一看就知道是干什么的,比如 public_method_defined?,就不废话了。总的来说,alias_method_chain 做了如下事情:
- 第3行,把原方法名末尾的标点符号(?, ! 和 =)去掉,存到 punctuation 变量中,因为标点符号出现在方法名中间是不合法的。
- 第4行,如果 alias_method_chain 后面还有 block,就调用 block (我以前还不知道可以这样用)。
- 第6到9行,用两个 alias_method 来改名,用上面的例子,就是把 name 变成 name_without_age,然后把 name_with_age 变成 name 。
- 最后部分,获取原方法的访问级别,然后设定新方法。