为类似的clases提取常见的ActiveRecord代码

时间:2013-07-10 00:35:35

标签: ruby ruby-on-rails-3 class inheritance composition

我定义了以下类,其中包含许多具有微小变化的常见代码。

class ThirdPartyComponent < ActiveRecord::Base
  belongs_to :prev_version, :class_name => 'ThirdPartyComponent', :foreign_key => 'prev_version_id'
  has_one :next_version, :class_name => 'ThirdPartyComponent', :foreign_key => 'prev_version_id'

  attr_accessible :name, :version, :installer, :install_script

  mount_uploader :installer, ComponentFileUploader
  mount_uploader :install_script, ComponentFileUploader

  validates :name, :presence => true
  validates :version, :presence => true, :format => { :with => /\A\d{1,3}\.\d{1,2}\z/ }
  validates :installer, :presence => true
  validates :install_script, :presence => true
  validate :increased_version

  def increased_version
    # Check to ensure that version number is greater than the previous version number for the same component set
    unless prev_version.nil?
      version > prev_version.version
    end
  end 

  def all_previous_versions
    prev_versions = all_versions
    prev_versions.shift
    prev_versions
  end

  def all_versions
    current_version = self
    all_versions = [current_version]
    while !current_version.prev_version.nil?
      all_versions << current_version.prev_version
      current_version = current_version.prev_version
    end
    all_versions
  end
end

class RegistryComponent < ActiveRecord::Base

  belongs_to :prev_version, :class_name => 'RegistryComponent', :foreign_key => 'prev_version_id'
  has_one :next_version, :class_name => 'RegistryComponent', :foreign_key => 'prev_version_id'

  attr_accessible :name, :version, :registry_file

  mount_uploader :registry_file, ComponentFileUploader

  validates :name, :presence => true
  validates :version, :presence => true, :format => { :with => /\A\d{1,3}\.\d{1,2}\z/ }
  validates :registry_file, :presence => true
  validate :increased_version

  def increased_version
    # Check to ensure that version number is greater than the previous version number for the same component set
    unless prev_version.nil?
      version > prev_version.version
    end
  end

  def all_previous_versions
    prev_versions = all_versions
    prev_versions.shift
    prev_versions
  end

  def all_versions
    current_version = self
    all_versions = [current_version]
    while !current_version.prev_version.nil?
      all_versions << current_version.prev_version
      current_version = current_version.prev_version
    end
    all_versions
  end
end

我也在考虑将来添加一些其他组件,同样具有非常相似的功能。

我想将这些类中的公共代码提取到一个文件中(包括ActiveRecord方法调用,如validates等),然后在具体类中引用它们。

到目前为止,我已经尝试过,

  1. 继承 - 我创建了一个继承自ActiveRecord的基类,然后创建了从基类继承的每个类。结果:Rails抱怨它找不到名称与基类匹配的数据库表。
  2. 继承 - 我考虑将基类创建为无表格模型(参见http://railscasts.com/episodes/219-active-model)但后来我意识到具体类也缺少完整的ActiveRecord功能
  3. 组合 - 我尝试在模块中定义公共代码,然后在具体类中使用include或extend来访问它,如下所示。

    module ComponentBase
      belongs_to :prev_version, :class_name => self.class.name, :foreign_key => 'prev_version_id'
      has_one :next_version, :class_name => self.class.name, :foreign_key => 'prev_version_id'
    
      attr_accessible :name, :version
    
      validates :name, :presence => true
      validates :version, :presence => true, :format => { :with => /\A\d{1,3}\.\d{1,2}\z/ }
      validate :increased_version
    
      def increased_version
        # Check to ensure that version number is greater than the previous version number for the same component set
        unless prev_version.nil?
          version > prev_version.version
        end
      end
    
      def all_previous_versions
        prev_versions = all_versions
        prev_versions.shift
        prev_versions
      end
    
      def all_versions
        current_version = self
        all_versions = [current_version]
        while !current_version.prev_version.nil?
          all_versions << current_version.prev_version
          current_version = current_version.prev_version
        end
        all_versions
      end
    end
    
    class RegistryComponent < ActiveRecord::Base
      include ComponentBase
    
      attr_accessible :registry_file
    
      mount_uploader :registry_file, ComponentFileUploader
    
      validates :registry_file, :presence => true
    end
    

    这导致了belongs_to未定义ComponentBase方法的错误。这看起来是最有希望的解决方案,但有没有办法在包含它们的类的上下文中执行ActiveRecord类方法?或者,是否有更好的方法来实现相同的目标?

3 个答案:

答案 0 :(得分:1)

你的第一个选择实际上是最好的选择。 Rails使用单表继承,这意味着所有子类的数据都保存在同一个表中,这就是您遇到错误的原因。

你应该做的是创建一个名为Component的新模型,并在其中添加所有组件中通用的所有字段以及一个名为type的额外字段,该字段应为字符串字段。

您的组件模型将具有所有常用字段,逻辑和验证。

class Component < ActiveRecord::Base
  ...
end

然后让每个组件类子类化为Component。

class ThirdPartyComponent < Component
  ...
end

答案 1 :(得分:0)

问题是您需要belongs_to方法在包含模块的类上运行,而不是模块本身。

查看包含http://www.ruby-doc.org/core-2.0/Module.html#method-i-included的模块#,这将允许您在包含模块的模块上运行代码。 Remmeber认为Module是Class的祖先,所以这适用于类和模块。

在这种情况下,您将希望在包含该模块的类上运行belongs_to,因此以下内容应作为您可以使用的示例:

module ComponentBase
    def self.included(mod)
        mod.class_eval do
            belongs_to :prev_version, :class_name => self.class.name, :foreign_key => 'prev_version_id'
        end
    end
end

答案 2 :(得分:0)

我在Extending a Ruby class with a standalone piece of code偶然发现了以下答案,并通过一些实验让它发挥作用。所以最终的代码是,

module ComponentBase

  def self.included(base)
    base.class_eval do
      belongs_to :prev_version, :class_name => base, :foreign_key => 'prev_version_id'
      has_one :next_version, :class_name => base, :foreign_key => 'prev_version_id'

      attr_accessible :name, :version

      validates :name, :presence => true
      validates :version, :presence => true, :format => { :with => /\A\d{1,3}\.\d{1,2}\z/ }
      validate :increased_version
    end
  end

  def increased_version
    # Check to ensure that version number is greater than the previous version number for the same component set
    unless prev_version.nil?
      version > prev_version.version
    end
  end

  def all_previous_versions
    prev_versions = all_versions
    prev_versions.shift
    prev_versions
  end

  def all_versions
    current_version = self
    all_versions = [current_version]
    while !current_version.prev_version.nil?
      all_versions << current_version.prev_version
      current_version = current_version.prev_version
    end
    all_versions
  end
end

class RegistryComponent < ActiveRecord::Base
  include ComponentBase

  attr_accessible :registry_file

  mount_uploader :registry_file, ComponentFileUploader

  validates :registry_file, :presence => true
end

解决方案是使用每次将模块包含在其他地方时调用的included回调。然后,在基础模块上调用class_eval以在类上下文中运行方法(即作为类方法)。最棘手的部分是在这个上下文中获取类名,但事实证明我可以使用base(不完全确定为什么会出现这种情况但它有效)。