去年有一次的周记分享过 ActiveSupport::Concern (以下简称 Concern) 的用法,但是对它的实现原理一直不太明白,周末把它的实现源码仔细研究了一番,总算比较深入了解了它的工作原理,同时也让我对 Ruby 元编程有了更深的了解。
Concern 主要用于解决 module mix 的依赖问题,同时简化 module mix 的步骤。 关于依赖问题,必须用一个例子才好说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
上面例子中 Host 为了 include Bar,必须得先 include Bar 依赖的 Foo,这是因为 Bar 在 include Foo 时,只是为 Bar extend method_injected_by_foo 方法,所以 Host 必须显式的 include Foo,才能够 extend method_injected_by_foo 方法。
使用 Concern 后,代码可以简写为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
Bar 和 Foo 都 extend ActiveSupport::Concern
后,Host include Bar 已经不需要事先 mix Foo。
这是怎么做到的呢,看看 Concern 的代码,其实代码很少:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
关键部分是 append_features
方法,通过阅读 ruby 的文档和源码得知,ruby 在 include 一个 module 时,实际会触发两个方法,一个是 append_features,进行实际的 mixing 操作,包括增加常量,方法和变量到模块中,另外一个是 included 方法,也就是我们常用来作为 include 钩子的方法,默认的 included 是一个空方法,我们通过重载它使钩子起作用。
从代码可知,当一个模块 extend ActiveSupport::Concern
时,将产生 3 个影响:
- 为模块设置了一个实例变量
变量为 @_dependencies,其值为空数组,表示依赖的模块,将在 append_features 中用到;
- append_features 方法被重载;
重载后行为有了很大变化,它的处理分两种情况:
一种是当它被一个有 @dependencies 实例变量的模块被 include 时,直接把自身加到 @dependencies 中,
比如当 Bar include Foo 时,将触发 Foo 的 append_features(base) 方法,此时 base 是 Bar,self 是 Foo,由于 Bar 已经 extend ActiveSupport::Concern
,Bar 的 @dependencies 有定义,所以直接把 Foo 加到 Bar 的 @dependencies 中,然后直接返回,没有立即执行 mixing 操作。
当 Host include Bar 时,将触发 Bar 的 append_features(base) 方法,此时
base 是 Host,self 是 Bar,Host 没有 extend ActiveSupport::Concern
,所以 Host 的 @dependencies 无定义,将执行下面的分支,首先 include Foo(通过 Bar 的 @dependencies 获得 ),然后 include Bar (通过 super),然后是后续操作。
- included 方法被重载;
included 的动作比较简单,如果以没有参数形式调用,将把 block 存放到 @included_block 变量中,@included_block 的 block 将在 append_features 方法中使用。
通过施加的这 3 个影响,ActiveSupport::Concern 完成了它的全部功能,非常简洁精练。
从中学到的东西,包括:
module 中 append_features 和 included 方法时 include 的核心;
可以通过覆盖这些方法改变操作的行为;
可以定义模块实例变量存储属于该模块的一些数据;