Ruby 中的动态 类、Class.new、解包 Sequel::Model

Dynamic classes in Ruby, Class.new, unpacking Sequel::Model

我是 ruby 的新手,正在尝试弄清楚下面 Sequel gem 中的 class 声明是如何在幕后工作的。

class Enumeration < Sequel::Model(DB[:enumerations]); end

快速调查 Sequel gem 的代码后,在我看来模块方法 Sequel::Model return 是一个 class 实例配置 class 属性。 Class的return实例然后在继承层次中使用,所以我试图通过代码测试我的理解;

module MySequel
  class MyModel
    module ClassMethods
      attr_accessor :table_name
  
      
      def model(table_name)
        klass = Class.new(self)
                
        klass.table_name = table_name
        
        puts klass.table_name # prints table_name, it does get set for the first class Class object klass
        
        klass
      end
    end
    
    extend ClassMethods
  end
end

class Enumeration < MySequel::MyModel.model(:enumerations); end
class EnumerationValue < MySequel::MyModel.model(:enumeration_values); end

p Enumeration.table_name        # prints nil
p EnumerationValue.table_name   # prints nil

根据我的理解,class 变量 table_name 在创建 Class 的实例时被设置并传播到它的子 class。然而,似乎并非如此。

有人可以解释 Sequel::Model 背后的概念以及我的示例实现中的问题以获得相同的结果。

此示例中的行为类似于使用上面的匿名 classes 时的行为。

class TestModel

  @@table_name = 'test'
  
  def TestModel.table_name
  @@table_name
  end
  
  def TestModel.table_name=(value)
  @@table_name = value
  end
end

TestModel.table_name = 'posts'
p TestModel.table_name    # prints posts


class ChildTestModel < TestModel; end

puts Enumeration.table_name   # prints nil

Sequel 库似乎通过调用模块方法向子 class 传播有关数据集集的信息(通过匿名 class。)

但是,我想不通; Ruby 语言的什么构造允许下面的状态 class 知道它仅限于 table 枚举中名称设置为 'status'.

的记录
class Status < Sequel::Model(DB[:enumerations].where(name: 'status')); end

因此,大部分代码都是正确的,您对它的工作原理的假设也是正确的。真是太棒了,绝对不是ruby的新手级别!

所以,什么不起作用。当你定义一些 class 的 subclass 时,subclass 继承了所有的“class 方法”(在引号中,因为实际上没有这样的东西),但是实例变量不被继承(即 - class Class 实例的实例变量):

class A
  @foo = 1
end

A.instance_variables #=> [:@foo]

class B < A; end

B.instance_variables #=> []

这意味着,如果您在 model 方法中创建的匿名 class 上调用 setter,它只会在 class 上设置实例变量.匿名 class 的子 classes 不会继承它。你可以看到这会起作用:

Enumeration = MySequel::MyModel.model(:enumerations)
EnumerationValue = MySequel::MyModel.model(:enumeration_values)

Enumeration.table_name        #=> :enumerations
EnumerationValue.table_name   #=> :enumeration_values

老实说,将您的匿名 class 转入此处确实没有太大收获。只有当您还缓存了那些 classes 并且对该方法的单独调用两次返回相同的 Class 对象时,这才有意义。

如果你仍然希望 table_name 是可继承的,你需要对此非常明确,而不是使用 attr_accessor,你需要直接查看 superclass :

attr_setter :table_name
def table_name
  @table_name || (superclass.table_name if superclass.respond_to?(:table_name))
end

您可能需要在此处考虑一些调整。以上版本

table 名称在定义 Enumeration 或 EnumerationValue 时设置,并保存在父 class(匿名 class)中。

与 javascript.

的原型继承相比,就我个人而言,掌握这个概念要简单得多

可以通过Enumeration和EnumerationValue的superclass访问class。

# frozen_string_literal: true

module MySequel
  class MyModel
    module ClassMethods
      attr_accessor :table_name
      
      def model(table_name)
        klass = Class.new(self)
                
        klass.table_name = table_name
        
        klass
      end
    end
    
    extend ClassMethods
  end
end

class Enumeration < MySequel::MyModel.model(:enumerations); end
class EnumerationValue < MySequel::MyModel.model(:enumeration_values); end

p Enumeration.superclass.table_name        # prints :enumerations
p EnumerationValue.superclass.table_name   # prints :enumeration_values

The Sequel library is seemingly propagating the information about the dataset set through call to module method to the child class

是的,的确如此。 Sequel 使用 inherited callback to copy the class instance variables over to the subclass (see lib/sequel/model/base.rb#843).

这是您的示例的一个非常基本的版本 class:

module MySequel
  class MyModel
    module ClassMethods
      attr_accessor :table_name

      def model(table_name)
        klass = Class.new(self)
        klass.table_name = table_name
        klass
      end

      def inherited(subclass)
        instance_variables.each do |var|
          value = instance_variable_get(var)
          subclass.instance_variable_set(var, value)
        end
      end
    end

    extend ClassMethods
  end
end

因此,子classes 将其 class 实例变量(此处 @table_name)设置为与(匿名)父 class:

class Enumeration < MySequel::MyModel.model(:enumerations); end
class EnumerationValue < MySequel::MyModel.model(:enumeration_values); end

p Enumeration.table_name      #=> :enumerations
p EnumerationValue.table_name #=> :enumeration_values