ActiveSupport 源码探索 -- aliasing

前言

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 做了如下事情:

  1. 第3行,把原方法名末尾的标点符号(?, ! 和 =)去掉,存到 punctuation 变量中,因为标点符号出现在方法名中间是不合法的。
  2. 第4行,如果 alias_method_chain 后面还有 block,就调用 block (我以前还不知道可以这样用)。
  3. 第6到9行,用两个 alias_method 来改名,用上面的例子,就是把 name 变成 name_without_age,然后把 name_with_age 变成 name 。
  4. 最后部分,获取原方法的访问级别,然后设定新方法。
posted @ 2011-05-23 20:52  darkbaby123  阅读(1149)  评论(0编辑  收藏  举报