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
执行后,Person
module
下的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
end
Concern
的源码非常简单,总共就定义了 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
end
Rails 对 included
进行的扩展其实很简单
included
继续作为include
的回调使用时,base
不为nil
,则执行super
included
作为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 Study
class_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)
end
class_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 Person
extended: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