Ruby 是如何实现多重继承的
前言
Ruby 用了也接近一年了,有很多黑魔法让我感到非常有意思,刚开始学习的时候也经常让我不知所措。
其中很有意思的一点就是 include 和 extend 这两个方法来变相实现 多重继承,即代码的复用。但是用归用,却没有细细的研究其中的实现,故此,今天好好的看一看。
include
include 方法在 Ruby 中会被 include 模块的方法添加为当前模块下的实例方法
module Person
def name
p 'My Name Is Person'
end
end
module User
include Person
end
User.new.name
# My Name Is Person这样,Person 就可以被随意引用,使用其中的实例方法 name。
然而,这是怎么做到的呢,定义在 Person 中的 name 为什么能被 User 的实例调用呢?
那这就需要看看 include 的源码了
class Module
# include(module, ...) -> self
#
# Invokes <code>Module.append_features</code> on each parameter in reverse order.
def include(module1, *smth)
# This is a stub, used for indexing
end
...append_features
Ruby 源码由 C 写成,所以我们只能看文档,文档中很清楚的指明了调用 include 方法会触发 append_features 方法,那我们就看看 append_features 的源码
# append_features(mod) -> mod
#
# When this module is included in another, Ruby calls
# <code>append_features</code> in this module, passing it the
# receiving module in _mod_. Ruby's default implementation is
# to add the constants, methods, and module variables of this module
# to _mod_ if this module has not already been added to
# _mod_ or one of its ancestors. See also <code>Module#include</code>.
def append_features(mod)
# This is a stub, used for indexing
end- 当一个
module作为mixin被include时,会默认调用append_features - 在
Ruby中append_features的默认实现是将mixin中的constants、methods和module variables添加到需要include的module中。 - 所以当
append_features执行后,Personmodule下的name被添加到了User中
验证
重写 append_features 方法:
module Person
extend ActiveSupport::Concern
def name
p 'My Name Is Person'
end
def self.append_features(base)
end
end
User.new.name
# undefined method `name' for #<User:0x00007f84b918c898> (NoMethodError)很显然,append_features 被覆盖,导致 name 没有被添加到 User 中
included:include 的回调
append_features 的实现满足了我们对实例方法的代码重用,但是没有实现对类方法的重用,如果我们想在 include 时同时导入类方法,或者其他操作,我们可以选择重写 append_features。但是在 Ruby 中并不建议直接重写 append_features,而是给我们提供了简单便捷的钩子方法 included。
# included(othermod)
#
# Callback invoked whenever the receiver is included in another
# module or class. This should be used in preference to
# <tt>Module.append_features</tt> if your code wants to perform some
# action when a module is included in another.
#
# module A
# def A.included(mod)
# puts "#{self} included in #{mod}"
# end
# end
# module Enumerable
# include A
# end
# # => prints "A included in Enumerable"
def included(othermod)
# This is a stub, used for indexing
end如果我们想要导入类方法,重写刚刚的 Person 类:
module Person
# 与 included 做对比
def self.run
p 'I Can Run'
end
def self.included(base)
base.class_eval do
def self.eat
p 'I Can Eat'
end
end
end
end
User.eat
# I Can Eat
User.run
# undefined method `run' for User:Class (NoMethodError)Rails 是如何扩展的
Rails 中有一个非常好用的库 ActiveSupport,对 Ruby 语言的很多特性都实现了扩展,今天要看的就是其中 ActiveSupport::Concern 是如何扩展 include 的
# 所有注释都被我删除了,有兴趣的请查看源码
module ActiveSupport
module Concern
class MultipleIncludedBlocks < StandardError
def initialize
super "Cannot define multiple 'included' blocks for a Concern"
end
end
def self.extended(base)
base.instance_variable_set(:@_dependencies, [])
end
def append_features(base)
if base.instance_variable_defined?(:@_dependencies)
base.instance_variable_get(:@_dependencies) << self
false
else
return false if base < self
@_dependencies.each { |dep| base.include(dep) }
super
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
end
end
def included(base = nil, &block)
if base.nil?
if instance_variable_defined?(:@_included_block)
if @_included_block.source_location != block.source_location
raise MultipleIncludedBlocks
end
else
@_included_block = block
end
else
super
end
end
def class_methods(&class_methods_module_definition)
mod = const_defined?(:ClassMethods, false) ?
const_get(:ClassMethods) :
const_set(:ClassMethods, Module.new)
mod.module_eval(&class_methods_module_definition)
end
end
endConcern 的源码非常简单,总共就定义了 3 个实例方法,1 个类方法,1 个错误,其中 append_features 和 included 方法刚刚讲过,我们来看看 Rails 做了什么
included:更优雅的 included
def included(base = nil, &block)
if base.nil?
if instance_variable_defined?(:@_included_block)
if @_included_block.source_location != block.source_location
raise MultipleIncludedBlocks
end
else
@_included_block = block
end
else
super
end
endRails 对 included 进行的扩展其实很简单
included继续作为include的回调使用时,base不为nil,则执行superincluded作为mixin中的类方法被调用,并传入一个block,则将该block存入@_included_block,并且不允许有多个@_included_block存在
这么做的目的是,使 mixin 可以通过 included 方法定义 def self.included,让代码更优雅
比如,如果使用 def slef.included 方法
module Student
def self.included(base)
base.class_eval do
def self.study
p 'I Can Study'
end
end
end
end
class User
include Student
end
User.study
# I Can Study而使用 included 方法就可以这样写
require 'active_support/concern'
module Student
extend ActiveSupport::Concern
included do
def self.study
p 'I Can Study'
end
end
end
class User
include Student
end
User.study
# I Can Studyclass_methods:更简洁的类方法定义
在上面我们看见了 included 是如何更优雅的实现类方法的定义的,而 Rails 还顺便提供了一个更简单的 magic 方法 class_methods
使用:
require 'active_support/concern'
module Foo
extend Concern
class_methods do
def run
'I Can Run'
end
end
end
class Bar
include Foo
end
p Bar.run
# I Can Run源码:
def class_methods(&class_methods_module_definition)
mod = const_defined?(:ClassMethods, false) ?
const_get(:ClassMethods) :
const_set(:ClassMethods, Module.new)
mod.module_eval(&class_methods_module_definition)
endclass_methods 的实现比 included 更简单,就是将 block 转换成一个新的 module ClassMethods,而如何注入则依赖于 append_features 来实现
append_features:解决内部依赖
Rails 使用 append_features 解决了一个非常重要的问题,那就是内部依赖的引入
module Foo
def self.included(base)
base.class_eval do
def self.test
'test'
end
end
end
end
module Bar
def self.included(base)
base.class_eval do
p test
end
end
end
class User
include Foo
include Bar
end如果 A mixin 依赖于 B mixin 中 included 定义的方法,那引入的时候就必须同时引入 A 和 B,这就要求开发者必须对每一个 mixin 的引入都要非常熟悉才能保证不出错。
而 ActiveSupport::Concern 通过 append_features 解决了这个问题
require 'active_support/concern'
module Foo
extend ActiveSupport::Concern
def self.included(base)
base.class_eval do
def self.test
'test'
end
end
end
end
module Bar
extend ActiveSupport::Concern
include Foo
def self.included(base)
base.class_eval do
p test
end
end
end
class User
include Bar
end
# test像这样的话,开发者只需要引入 Bar,而不再需要管 Bar 中 include 了几个模块。
而 Rails 中是如何这么聪明的实现的呢,源码如下:
# 1. Foo 和 Bar 使用 extend ActiveSupport::Concern 时,定义实例变量 @_dependencies = []
def self.extended(base) #:nodoc:
base.instance_variable_set(:@_dependencies, [])
end
def append_features(base)
if base.instance_variable_defined?(:@_dependencies)
# 2. 当 Bar include Foo 时触发 Foo 的 append_features,发现 Bar 已定义实例变量 @_dependencies
# 3. 将当前模块 Foo 添加入 Bar 的 @_dependencies
base.instance_variable_get(:@_dependencies) << self
false
else
return false if base < self
# 4. 当 User include Bar 时 @_dependencies 中的所有模块(Foo)被 User 同时 include
@_dependencies.each { |dep| base.include(dep) }
super
# 配合 class_methods 方法实现类方法的注入
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
# 配合 included 方法实现 self.included
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
end
end而 append_features 除了解决了内部依赖的引入,还实现了两个 magic 方法 included 和 class_methods 的注入
extend
extend 方法在 Ruby 中会被 extend 模块的方法添加为当前模块下的类方法
module Person
def name
"My name is Person"
end
end
class User
extend Person
end
p User.name
# My name is Personextended:extend 的回调
在 extend 之后如果需要执行某些回调,则只需要定义 self.extended
module Person
def self.extended(base)
p "#{base} extended #{self}"
end
def name
"My name is Person"
end
end
class User
extend Person
end
# User extended Person