求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
Ruby的模块包含机制
 
作者 Oleksandr Reminnyi,火龙果软件    发布于 2014-04-23
 

在 Ruby 中,模块可以被其他模块包含(这里,其他模块也包括类,因为类也是模块)。通过包含模块,可以构建出复杂的类层次结构。本文解释了当包含一个模块时,Ruby 内部都做了哪些实际的工作。

下面这段 Ruby 代码创建了 A, B, C 三个模块,C 包含 A 和 B。

A = Module.new

B = Module.new

module C

include A, B # Inclusion is done by this line

end

在模块 C 中,语句 include A, B 完成了模块包含的工作,该语句以 A, B 两个模块为作为参数,调用了模块 C 的 include 方法。一切都那么自然。

现在,把注意力放在 include 方法上,它的默认实现是 Module#include。根据 API 文档,它以相反的顺序对参数逐一调用 append_features 方法。对应的 C 函数是,

rb_mod_include。
static VALUE
rb_mod_include(int argc, VALUE *argv, VALUE module)
{
int i;
ID id_append_features, id_included;
CONST_ID(id_append_features, "append_features");
CONST_ID(id_included, "included");
for (i = 0; i < argc; i++)
Check_Type(argv[i], T_MODULE);
while (argc--) {
rb_funcall(argv[argc], id_append_features, 1, module);
rb_funcall(argv[argc], id_included, 1, module);
}
return module;
}

从 Ruby 源码来看,除了对参数模块调用 append_features 方法外,还紧接着调用了 included 方法。所以,文档描述得并不完整,可能还没有更新。

append_features 和 included 都是回调方法。而 append_features 方法才是真正干活的地方,Ruby 通过这个方法将包含的逻辑转移到了被包含的模块上。这种精巧的设计能让你灵活地自定义包含模块的行为,既可以在发出包含动作的模块中重定义 include 方法,也可以从源头着手,重定义 append_features 方法。

再来看看 append_features 的默认实现 Module#append_features 都做了什么。下面是 API 文档上给出的源码:

rb_mod_append_features(VALUE module, VALUE include)
{
switch (TYPE(include)) {
case T_CLASS:
case T_MODULE:
break;
default:
Check_Type(include, T_CLASS);
break;
}
rb_include_module(include, module);
return module;
}

这个函数比较简单,除了类型安全检查,它只是 rb_include_module 函数的封装,我们进一步跟到 rb_include_module 函数中去看看。

void
rb_include_module(VALUE klass, VALUE module)
{
int changed = 0;
rb_frozen_class_p(klass);
if (!OBJ_UNTRUSTED(klass)) {
rb_secure(4);
}
if (!RB_TYPE_P(module, T_MODULE)) {
Check_Type(module, T_MODULE);
}
OBJ_INFECT(klass, module);
changed = include_modules_at(klass, RCLASS_ORIGIN(klass), module);
if (changed < 0)
rb_raise(rb_eArgError, "cyclic include detected");
if (changed) rb_clear_cache();
}

同样,rb_include_module 函数也没有做多少实际的工作,只是做了一些安全方面的检查,然后把主要工作交给了 include_modules_at 函数。

static int
include_modules_at(const VALUE klass, VALUE c, VALUE module)
{
VALUE p;
int changed = 0;
const st_table *const klass_m_tbl = RCLASS_M_TBL(RCLASS_ORIGIN(klass));
while (module) {
int superclass_seen = FALSE;
if (RCLASS_ORIGIN(module) != module)
goto skip;
if (klass_m_tbl && klass_m_tbl == RCLASS_M_TBL(module))
return -1;
/* ignore if the module included already in superclasses */
for (p = RCLASS_SUPER(klass); p; p = RCLASS_SUPER(p)) {
switch (BUILTIN_TYPE(p)) {
case T_ICLASS:
if (RCLASS_M_TBL(p) == RCLASS_M_TBL(module)) {
if (!superclass_seen) {
c = p; /* move insertion point */
}
goto skip;
}
break;
case T_CLASS:
superclass_seen = TRUE;
break;
}
}
c = RCLASS_SUPER(c) = rb_include_class_new(module, RCLASS_SUPER(c));
if (FL_TEST(klass, RMODULE_IS_REFINEMENT)) {
VALUE refined_class =
rb_refinement_module_get_refined_class(klass);
st_foreach(RMODULE_M_TBL(module), add_refined_method_entry_i,
(st_data_t) refined_class);
FL_SET(c, RMODULE_INCLUDED_INTO_REFINEMENT);
}
if (RMODULE_M_TBL(module) && RMODULE_M_TBL(module)->num_entries)
changed = 1;
if (RMODULE_CONST_TBL(module) && RMODULE_CONST_TBL(module)->num_entries)
changed = 1;
skip:
module = RCLASS_SUPER(module);
}
return changed;
}

include_modules_at 函数做了许多工作,值得好好分析一下。

首先,最外层的 while 循环对 module 的祖先链(ancestors)逐一遍历。对于每个 module,内层的 for 循环会检查 module 是否已经包含在 klass 的祖先链中,如果已经包含,则跳过,否则,把 module 插入到 c 和 c 的父类之间。

在测试 module 是否已包含在祖先链的过程中,变量 superclass_seen 用来判断是否越过了一个非包含类,如果是,则不移动插入点。所以模块包含不会将模块插到祖先链中下一个非包含类之后。

模块的插入工作是由第 31 行代码完成的,它调用 rb_include_class_new 函数为模块创建了一个包含类(include class),它以 c 的父类作为父类,接着又将这个包含类设置为 c 的父类。我们来看看 rb_include_class_new 函数是如何创建包含类的。

VALUE
rb_include_class_new(VALUE module, VALUE super)
{
VALUE klass = class_alloc(T_ICLASS, rb_cClass);
if (BUILTIN_TYPE(module) == T_ICLASS) {
module = RBASIC(module)->klass;
}
if (!RCLASS_IV_TBL(module)) {
RCLASS_IV_TBL(module) = st_init_numtable();
}
if (!RCLASS_CONST_TBL(module)) {
RCLASS_CONST_TBL(module) = st_init_numtable();
}
RCLASS_IV_TBL(klass) = RCLASS_IV_TBL(module);
RCLASS_CONST_TBL(klass) = RCLASS_CONST_TBL(module);
RCLASS_M_TBL(klass) = RCLASS_M_TBL(RCLASS_ORIGIN(module));
RCLASS_SUPER(klass) = super;
if (RB_TYPE_P(module, T_ICLASS)) {
RBASIC(klass)->klass = RBASIC(module)->klass;
}
else {
RBASIC(klass)->klass = module;
}
OBJ_INFECT(klass, module);
OBJ_INFECT(klass, super);
return (VALUE)klass;
}

第 4 行,class_alloc 创建了一个新类,并为它设置了 T_ICLASS 标识,有了这个标识,Ruby 就会认为它是一个包含类。接着就是一系列的初始化,将新类的内部表指向模块中对应的表。最后,将新类的 klass 指向模块,这样,包含类就创建完成了。

第 17 行有个 RCLASS_ORIGIN 宏,我们来看看它的作用是什么,Ruby 源码中,该宏的定义如下:

 #define RCLASS_ORIGIN(c) (RCLASS_EXT(c)->origin)

可以看到,它指向一个类的 origin 成员。简单来说,它获取某个类的原始类。原始类跟模块前置有着密切的关系。关于模块前置的更多信息,请阅读《Ruby 中模块前置的实现》这篇文章。

了解这些信息之后,我们来总结一下。为了便于描述,我们以文章开头的那段代码作为例子,来回顾一下模块包含的全过程。

A = Module.new
  B = Module.new
  module C
  include A, B # Inclusion is done by this line
  end

当调用模块 C 的 include 方法时,默认实现 Module#include 会以相反的顺序对参数模块回调 append_features 和 included 方法。真正的包含逻辑在 Module#append_features 的内部实现。

对每个被包含的模块,Ruby 会遍历其祖先链。对链上的每个模块,会首先检查是否已包含过,如果已包含,则跳过,否则为该模块创建一个包含类,插入到模块 C 的祖先链中。

当然,以上的描述省略了一些细节,比如类型安全检查、对插入点的调整、检查循环包含、以及对 Refinement 的处理。如果读者有兴趣,可以参照源代码去理解。

模块包含的本质是将被包含的模块的祖先链插入到另一个模块的祖先链中,其实最终,都是在维护一个祖先链。

相关文章

微服务测试之单元测试
一篇图文带你了解白盒测试用例设计方法
全面的质量保障体系之回归测试策略
人工智能自动化测试探索
相关文档

自动化接口测试实践之路
jenkins持续集成测试
性能测试诊断分析与优化
性能测试实例
相关课程

持续集成测试最佳实践
自动化测试体系建设与最佳实践
测试架构的构建与应用实践
DevOps时代的测试技术与最佳实践
 
分享到
 
 
     


LoadRunner性能测试基础
软件测试结果分析和质量报告
面向对象软件测试技术研究
设计测试用例的四条原则
功能测试中故障模型的建立
性能测试综述
更多...   


性能测试方法与技术
测试过程与团队管理
LoadRunner进行性能测试
WEB应用的软件测试
手机软件测试
白盒测试方法与技术


某博彩行业 数据库自动化测试
IT服务商 Web安全测试
IT服务商 自动化测试框架
海航股份 单元测试、重构
测试需求分析与测试用例分析
互联网web测试方法与实践
基于Selenium的Web自动化测试
更多...