Key Points about ActiveSupport::Concern

    xiaoxiao2021-03-25  108

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

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

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

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

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

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

    module ActAsActivable def self.included(base) base.send(:include, InstanceMethods) base.extend ClassMethods base.class_eval do scope :active, where(is_active: true) end end module InstanceMethods def active? is_active end end module ClassMethods def all_active(reload = false) @all_active = nil if reload @all_active ||= active.all end end end

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

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

    module A def self.test_a end end class B extend A end class C include A end A.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 ActAsActivable extend ActiveSupport::Concern included do |base| scope :active, -> {where(is_active: true)} end module ClassMethods def all_active(reload = false) @all_active = nil if reload @all_active ||= active.all end end # instance methods def active? is_active end end

    解决 module 之间的依赖

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

    module Foo def self.included(base) base.class_eval do def self.method_injected_by_foo end end end end module Bar def self.included(base) base.method_injected_by_foo end end class Host include Foo # We need to include this dependency for Bar include 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 Foo extend ActiveSupport::Concern included do class_eval do def self.method_injected_by_foo ... end end end end module Bar extend ActiveSupport::Concern include Foo included do self.method_injected_by_foo end end class Host include Bar # works, Bar takes care now of its dependencies end

    原理剖析

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

    module Concern 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 return false else return false if base < self @_dependencies.each { |dep| base.send(:include, dep) } super base.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.", caller end base.class_eval(&@_included_block) if instance_variable_defined?("@_included_block") end end def included(base = nil, &block) if base.nil? @_included_block = block else super end end end

    可以看到,只定义了三个方法:self.extended,append_features和included。

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

    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 的extended和extended_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::Concern,Bar 的 @dependencies 有定义,所以直接把 Foo 加到 Bar 的 @dependencies 中,然后直接返回,没有立即执行 mixing 操作。 - 另一种是没有@dependencies定义的时候,也就是被没有 extend ActiveSupport::Concern的类 include 时。例如,当 Host include Bar 时,将触发 Bar 的 append_features(base) 方法,此时 base 是 Host,self 是 Bar,Host 没有 extend ActiveSupport::Concern,所以 Host 的 @dependencies 无定义,将执行下面的分支,首先 include Foo(通过 Bar 的 @dependencies 获得 ),然后 include Bar (通过 super),然后是后续操作。 3. included方法被重写。添加了新的功能 - 如果方法调用的时候没有参数,则将代码块的逻辑放入@_included_block中。之所以放入@_included_block是为了可以在发生依赖的时候,可以逐级调用所有模块的block方法。

    转载请注明原文地址: https://ju.6miu.com/read-35358.html

    最新回复(0)