如果包含类/模块,则只允许模块定义方法

时间:2014-11-14 20:56:07

标签: ruby module include active-model-serializers

我对ActiveModel的序列化有很多乐趣,特别是as_jsonserializable_hash的纠结网络。

我的应用有大量模型,通过包含模块来共享行为,我们称之为SharedBehavior

我的团队已经决定我们有一个默认格式,我们希望所有这些类在转换为JSON时都要遵循(用于在Rails应用程序中渲染),但是其中一些应该表现得有些不同。由于ActiveModel库中这两种方法的奇怪行为,在模型中添加列入白名单或列入黑名单的属性会被此模块中的方法定义覆盖,然后传递给ActiveModel中的超级声明。

出于这个原因,我希望这个模块只将这些方法的定义应用于模型,如果它们没有在那些模型中明确地重写(实质上,将模块从祖先链中取出用于一些方法)调用),但我仍然需要这个模块的共享行为。

我尝试通过有条件地解决这个问题,在IRB中动态应用模块包含的方法:

class A
  def foo
    puts 'in A'
  end
end

module D
  def self.included(base)
    unless base.instance_methods(false).include?(:foo)
      define_method(:foo) do
        puts 'in D'
        super()
      end
    end
  end
end

class B < A
  include D
end

class C < A
  include D
  def foo
    puts 'in C'
    super
  end
end

通过此声明,我预计C.new.foo的输出为

in C
in A

但它改为

in C
in D
in A

我唯一的另一个想法是将这个逻辑移到另一个模块中并在每个类中包含该模块(其中大约有54个)没有显式覆盖此方法,但是有一些缺点:

  1. 它在项目中引入了一些隐式耦合,如果它不想覆盖此方法实现,则新模型包含此模块
  2. 模块中这些序列化方法的当前实现与该模块建立的行为和属性有关,所以我觉得让第二个模块了解并依赖于{{{{{ 1}},虽然第二个模块几乎与第一个模块无关。
  3. 其他人是否可以想到另一个解决方案,或者在上面的代码示例中发现我的疏忽,这样我就可以在SharedBehavior挂钩中进行调用? (我也尝试切换included类定义C方法并包含foo模块的顺序,但看到了完全相同的行为。)

1 个答案:

答案 0 :(得分:3)

这里有两个棘手的错误。

  1. Ruby 评估类,因此表达式的顺序很重要。您在include D中定义foo之前C ,所以当调用included挂钩时,foo将不会被定义base。您需要在班级的结束 include D
  2. 您在foo中定义了D。因此,在D中包含B之后,D#foo已定义,即使您修复了上一个错误,也意味着仍然包含在C中。您需要base成为define_method
  3. 的接收者

    有一个有趣的转折:修复第二个错误使第一个错误无关紧要。通过直接在foo中定义base,它将被任何后续定义覆盖。这就像在做

    class C < A
      def foo
        puts 'in D'
        super()
      end
    
      # overwrites previous definition!
      def foo
        puts 'in C'
        super
      end
    end
    

    总而言之,你只需要

    # in D.included
    base.class_eval do
      define_method(:foo) do
        puts 'in D'
        super()
      end
    end