ActiveSupport::Concern 模块是 Ruby 中很常用,且很重要的一个模块。它鼓励抽取可重用逻辑放到不同的concern里面,规范了 module mix 的代码风格,并且解决了模块加载之间的依赖问题。

  • 鼓励公用逻辑抽取规范代码风格
  • 解决 module 之间的依赖
  • 原理剖析

鼓励公用逻辑抽取,规范代码风格

例如我们有 Post 和 Advertiser 两个类,这两个类拥有相同的判断是否是活跃状态的代码逻辑,scop、实例方法、类方法:

scope :active, -> {where(is_active: true)}def active?is_active
enddef self.all_active(reload = false)@all_active = nil if reload@all_active ||= active.all
end

为了重用这部分代码,我们将其抽取出来,放到一个module 中,如下:

module ActAsActivabledef self.included(base)base.send(:include, InstanceMethods)base.extend ClassMethodsbase.class_eval doscope :active, where(is_active: true)endendmodule InstanceMethodsdef active?is_activeendendmodule ClassMethodsdef all_active(reload = false)@all_active = nil if reload@all_active ||= active.allendend
end

ActAsActivable model 中,为了使该module被 include 时可以为类添加这几个方法,我们将scope 实例方法和类方法写入module中,并分别用 InstanceMethodsClassMethods包裹,并利用 hook 方法在被 include 的时候为新类添加新方法。

注意:
- 对于实例方法,我们完全可以不用InstanceMethods模块来包裹,当它们被 include 的或者 extend 的时候,它们会自动成为新类的实例方法或类方法。
- 而类方法无论如何定义,都无法自动成为新类的类方法,看下面几个例子:

module Adef self.test_aend
endclass Bextend A
endclass Cinclude A
endA.test_a # nil
B.test_a # NoMethodError: undefined method `test_a' for B:Class
C.test_a # NoMethodError: undefined method `test_a' for C:Class
C.new.test_a # NoMethodError: undefined method `test_a' for #<C:0x007fc0f629b5d0>
  • 对于 module 中定义的实例方法,可以通过 include 和 extend 使其成为实例方法或者类方法。但是如果同一个module中,即有类方法,又有实例方法方法,此时简单的 include 或者 extend 无法满足为类同时添加这两类方法的需求。此时我们只能通过添加 include hook 方法来实现。

而添加 include hook 的方式显得十分繁琐和臃肿。而使用 concern 则能很优雅的解决这些。
通过在 ActAsActivable include Concern模块,只需要按正常的方式定义实例方法,并将类方法包裹到 ClassMethods 模块,scope 方法写入 include do 模块里,并在需要它的地方使用 include ActAsActivable即可。

module ActAsActivableextend ActiveSupport::Concernincluded do |base|scope :active, -> {where(is_active: true)}endmodule ClassMethodsdef all_active(reload = false)@all_active = nil if reload@all_active ||= active.allendend# instance methodsdef active?is_activeend
end

解决 module 之间的依赖

下面示例来自于 lib/active_support/concern.rb。

module Foodef self.included(base)base.class_eval dodef self.method_injected_by_fooendendend
end
module Bardef self.included(base)base.method_injected_by_fooend
end
class Hostinclude Foo # We need to include this dependency for Barinclude Bar # Bar is the module that Host really needs
end

Bar模块依赖于Foo模块,如果我们需要在Host中使用Bar,如果直接 include Bar, 会报找不到 method_injected_by_foo的错误,所以我们必须在它之前 include Foo模块。而这并不是我们希望看到的。
通过引入Concern模块,我们可以不用担心模块依赖的问题。

require 'active_support/concern'
module Fooextend ActiveSupport::Concernincluded doclass_eval dodef self.method_injected_by_foo...endendend
end
module Barextend ActiveSupport::Concerninclude Fooincluded doself.method_injected_by_fooend
end
class Hostinclude Bar # works, Bar takes care now of its dependencies
end

原理剖析

Concern 源代码非常简单,只有短短三十余行:

module Concerndef self.extended(base)base.instance_variable_set("@_dependencies", [])enddef append_features(base)if base.instance_variable_defined?("@_dependencies")base.instance_variable_get("@_dependencies") << selfreturn falseelsereturn false if base < self@_dependencies.each { |dep| base.send(:include, dep) }superbase.extend const_get("ClassMethods") if const_defined?("ClassMethods")if const_defined?("InstanceMethods")base.send :include, const_get("InstanceMethods")ActiveSupport::Deprecation.warn "The InstanceMethods module inside ActiveSupport::Concern will be " \"no longer included automatically. Please define instance methods directly in #{self} instead.", callerendbase.class_eval(&@_included_block) if instance_variable_defined?("@_included_block")endenddef included(base = nil, &block)if base.nil?@_included_block = blockelsesuperendendend

可以看到,只定义了三个方法:self.extended,append_featuresincluded

Note:
- 当一个 module 被 include 的时候,会自动调用该 module 的append_featuresincluded 方法:

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");rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS);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;
}
  • 当一个 module 被 extend 的时候,会自动调用该 module 的extendedextended_object方法。

    static VALUE
    rb_obj_extend(int argc, VALUE *argv, VALUE obj)
    {int i;ID id_extend_object, id_extended;CONST_ID(id_extend_object, "extend_object");CONST_ID(id_extended, "extended");rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS);for (i = 0; i < argc; i++)Check_Type(argv[i], T_MODULE);while (argc--) {rb_funcall(argv[argc], id_extend_object, 1, obj);rb_funcall(argv[argc], id_extended, 1, obj);}return obj;
    }

当模块 Foo extends Concern 时,会发生三件事情:
1. extended:为 Foo设置了一个实例变量组 @_dependencies,里面用来存放Foo依赖的所有其他的模块。注意,@_dependencies是实例变量,并不是类变量。
2. append_features方法被重写。重写后行为有了很大变化,它的处理分两种情况:
- 一种是当它被一个有 @dependencies 实例变量的模块,也就是一个 extend 过ActiveSupport::Concern的模块 include 时,直接把自身加到 @dependencies 中。 比如当 Bar include Foo 时,将触发 Foo 的 append_features(base) 方法,此时 base 是 Bar,self 是 Foo,由于 Bar 已经 extend ActiveSupport::ConcernBar@dependencies 有定义,所以直接把 Foo 加到 Bar 的 @dependencies 中,然后直接返回,没有立即执行 mixing 操作。
- 另一种是没有@dependencies定义的时候,也就是被没有 extend ActiveSupport::Concern的类 include 时。例如,当 Host include Bar 时,将触发 Bar 的 append_features(base) 方法,此时 base 是 Host,self 是 BarHost 没有 extend ActiveSupport::Concern,所以 Host@dependencies 无定义,将执行下面的分支,首先 include Foo(通过 Bar 的 @dependencies 获得 ),然后 include Bar (通过 super),然后是后续操作。
3. included方法被重写。添加了新的功能 - 如果方法调用的时候没有参数,则将代码块的逻辑放入@_included_block中。之所以放入@_included_block是为了可以在发生依赖的时候,可以逐级调用所有模块的block方法。

Key Points about ActiveSupport::Concern相关推荐

  1. ActiveSupport::Concern 和 gem 'name_of_person'(300✨) 的内部运行机制分析

    理解ActiveRecord::Concern: 参考:include和extend的区别: https://www.cnblogs.com/chentianwei/p/9408963.html 传统 ...

  2. Key Points Estimation and Point Instance

    Abstract 在交通线路检测的情况下,需要考虑一个基本的感知模块,但需要考虑许多条件,如交通线路的数量和目标系统的计算能力.为了解决这些问题,本文提出了一种交通线路检测方法,即点实例网络(PINe ...

  3. Key Points Estimation and Point InstanceSegmentation Approach for Lane Detection 论文精读

    目录 用于车道检测的关键点估计和点实例分割方法 摘要 一.介绍 二.相关工作 三.方法 个人总结 论文地址 代码复现 参考链接 SGPN 用于车道检测的关键点估计和点实例分割方法 摘要 自动驾驶的感知 ...

  4. Key Points on Innovation from Peter Drucker

    Creating Your Tomorrow - 4 Questions from Peter Drucker 1. What do you have to abandon to create roo ...

  5. OpenGL Shader Key Points (3)

    Shader和Program Program Link过后,Shader就可以从Program中Detach并删掉.这样是不是可以节省一点点显存呢? 链接到同一个program的vertex和frag ...

  6. CenterNet:Objects as Points论文阅读笔记

    CenterNet论文阅读笔记 (一)Title (二)Summary (三)Research Objective (四)Problem Statement (五)Method 5.1 Loss Fu ...

  7. 谷歌数字图书馆_如何在没有联系的情况下找到6位数字的工作-提示使我获得了Google和其他技术巨头的工作机会...

    谷歌数字图书馆 Shortly after college, I began chasing something many people want but few ever get: a job th ...

  8. Rails5 Controller Document

    更新: 2017/06/28 大致完成全部 更新: 2017/06/29 补充module文件命名规则 更新: 2017/07/09 补充session的设置 更新: 2018/03/06 修正ren ...

  9. AI:《Why is DevOps for Machine Learning so Different?—为什么机器学习的 DevOps 如此不同?》翻译与解读

    AI:<Why is DevOps for Machine Learning so Different?-为什么机器学习的 DevOps 如此不同?>翻译与解读 目录 <Why is ...

最新文章

  1. 关于 线程模型中经常使用的 __sync_fetch_and_add 原子操作的性能
  2. Gerapy分布式管理框架
  3. 基于angular4+ng-bootstrap+bootstrap+scss的后台管理系统界面
  4. winform 配置文件的加密解密
  5. Invalid bound statement (not found):出现的原因和解决方法
  6. Springboot 项目中 xml文件读取yml 配置文件
  7. 面试问题:SpringMVC的执行流程
  8. Java编程的逻辑 (43) - 剖析TreeMap
  9. python从文件中读取数据时出现错误_python-从文件中读取数据
  10. Linux系统文件夹权限475,linux系统中文件的特殊权限
  11. Grid使用 ComboBox Binding DateTime Format WPF
  12. arm开发板上找不到/dev/i2c-*设备
  13. [转]什么是lib文件,lib和dll的关系如何
  14. AngularJs自定义指令详解(10) - 执行次序
  15. word插入目录右边对不齐
  16. 使用 docker 来安装 oracle 11c
  17. 计算机单元测验2符号化,地理信息系统概论
  18. 位置不可用无法访问介质受写入保护怎么修复?
  19. Bitmap、BitSet、RoaringBitmap持久化存储
  20. 【问题】SQL远程过程调用失败

热门文章

  1. Sqoop1和Sqoop2的刨析对比
  2. 基于微信小程序的流动人口管理移动APP设计与实现-计算机毕业设计源码+LW文档
  3. JAVA社招,让老板心动的简历原来是这样
  4. 软件发明专利实例_申请软件发明专利的一些案例
  5. 金立创始人刘立荣:从南下淘金到身价15亿
  6. 华为Ascend昇腾CANN详细教程(二)
  7. 裸辞接单第一个月的收入
  8. Linux搭建迅搜( Xunsearch )
  9. MTK平台的LCM防静电(esd-check)机制
  10. Android GB905协议详解