Ruby 理解 Ruby 中的 include 和 prepend

lvjian700 · 2016年01月12日 · 最后由 qustmath 回复于 2020年11月28日 · 8927 次阅读

Ruby 中使用 mixin 优雅的解决了 multiple inheritance 问题。在 Java 世界中使用 interface 解决这个问题,在 Ruby 中使用 module,与 Java 中不同的是:Ruby 中的 module 不但可以定义接口,而且还能提供实现

在 Ruby 提供 include 和 prepend 两种方式使用 module,而对 include 或者 prepend module 这种方式,Ruby 称为 mixin。本文将介绍这两种不同的 mixin 方式。

先认识一下 include 和 prepend

猜一下这段代码的输出:

class SuperSpeaker
  def speak
    puts 'Super class speaking...'
  end
end

module Chinese
  def speak
    puts '在说中文...'
    super
  end
end

module Thai
  def speak
    puts 'ฉันกำลังพูด...'
    super
  end
end

class Speaker < SuperSpeaker
  include Chinese
  prepend Thai

  def speak
    puts 'Subclass speaking...'
    super
  end
end

p Speaker.ancestors
speaker = Speaker.new
speaker.speak

这段代码将输出:

[Thai, Speaker, Chinese, SuperSpeaker, Object, Kernel, BasicObject]
ฉันกำลังพูด...
Speaker is speaking...
在说中文...
Super speaker is speaking...

通过输出我们可以看到:

  • include 会将 Chinese module 加入到 Speaker 的后面
  • prepend 会将 Thai module 加入到 Speaker 的前面

从 Ruby 的 Method lookup 机制理解 include 和 prepend

在理解 include 和 prepend 时,我们需要知道,当我在一个 instance 上调用方法时,都发生了什么?

  1. Ruby 中方法存在 Class 中,instance 只有变量没有方法。当在 instance 上调用方法时,会先从 instance 对应的 Class 中查找(先向右查找)
  2. 当无法在当前 Class 找到改方法时,Ruby 会按照继承链向上查找方法(后向上查找)

这个机制在 Ruby 中称为 Method lookup,一句话总结 Method lookup 机制:

『先向右,后向上』

在上段代码中,我们输出了 Speaker 的继承链:

p Speaker.ancestors
# => [Thai, Speaker, Chinese, SuperSpeaker, Object, Kernel, BasicObject]

Method lookup 过程是这样的:

method lookup

从 Method lookup 角度来理解 include 和 prepend:

  • include 将 Chinese module 加入到 Speaker 继承链的上方
  • prepend 将 Thai module 加入到 Speaker 继承链的下方

这样也就解释了,为什么 Speaker#speak 会先输出泰文:『ฉันกำลังพูด...』。

使用 include 和 prepend 实现一个简单的 DSL

在 Rails controller 中,before_action 非常好用,这里我们使用 include 和 prepend 实现一个简单的 before_action:

module BeforeAction
  def self.included(klass)
    klass.extend(ClassMethods)
  end

  module ClassMethods
    def before_action(method_name, options)
      action_module = Module.new do
        send :define_method, options[:for] do |*args, &block|
          send method_name

          super(*args, &block)
        end
      end

      prepend action_module
    end
  end
end

class Speaker
  include BeforeAction

  before_action :chinese_self_intro, for: :speak

  def speak
    puts 'I am speaking...'
  end

  private

  def chinese_self_intro
    puts 'Hello, I come from china.'
  end
end

Speaker.new.speak

输出一下 Speaker 的继承链,便于我们理解 before_action 的原理:

p action_module.class_methods
#=> [:speak]
p Speaker.ancestors
#=> [#<Module:0x007fe1ca254be0>, Speaker, BeforeAction, Object, Kernel, BasicObject]

before action

  1. 使用 include 将 BeforeAction mixin 到 Speaker 继承链上面,此时在定义 Speaker 时便可使用 before_action
  2. 在 before_action 中 prepend 将带有 speak 方法的 Anonymous module,将 Anonymous module 插入到继承链的底端。这样在调用 Speaker#speak 时,先调用 Anonymous module 中的 speak
  3. 在 Anonymous module 中的 speak 方法调用 for 参数指定的 chinese_self_intro
  4. 在 Anonymous module 中的 speak 方法的最后,使用 super(*arg, &block) 调用继承链上方的方法(即:Speaker#speak

总结

Ruby 中的 include 和 prepend 为 Ruby 提供了 mixin 机制,这个机制不但解决 multiple inheritance 问题,而且为 Ruby 的 DSL 能力提供了强大的支持。

那么什么时候用 mixin,什么时候用 inheritance?

从一个 Java 程序员角度来讲,当你想定义 interface 时便可使用 mixin。

我的理解就是当一个 Speaker 的实例对象调用类 Speaker 中的 speak 方法时,根据 Ruby 的 Method lookup 机制, 先寻找到的是 action_module 中的 speak 方法。 因为我们之前在 action_module 中通过 define_method,动态的定义了一个 speak 方法,这个方法的实现就在代码块中。

send :define_method, options[:for] do |*args, &block|
  send method_name
  super(*args, &block)
end

所以先去调用 method_name 指代的方法,然后携带参数 (如果有的话),继续调用继承链上一级也就是类 Speaker 的 speak 方法。 这样就实现了 before_action 机制。

觉得 Ruby 真是自带鸡汤 before_action 这一段代码信息量不小。

代码高亮一下会不会好点

#2 楼 @qinfanpeng 赞,忘了可以指定语法高亮

#1 楼 @nju520 恩,是这个原理。 最后少解释了super这一段,已添加。

直接把 before_action 写到 BeforeAction 模块里面,然后直接在 Speaker 里 extend BeforeAction 好像也没有什么问题

besfan 分享一次排查问题的过程 提及了此话题。 06月28日 17:30

这么酷炫的技巧...

def before_action(method_name, options)
  action_module = Module.new do
    send :define_method, options[:for] do |*args, &block|
      send method_name

      super(*args, &block)
    end
  end

  prepend action_module
end

include BeforeAction

请问这里用 prepend 为什么报错呢

非常感谢,理解了 before_action 实现原理了。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号