Rails STI类自动初始化

时间:2015-09-24 22:00:18

标签: ruby-on-rails ruby ruby-on-rails-4 sti

我正在尝试创建一个STI Base模型,该模型会自动更改为继承的类:

#models/source/base.rb
class Source::Base < ActiveRecord::Base
  after_initialize :detect_type

  private
  def detect_type
    if (/(rss)$/ ~= self.url)
      self.type = 'Source::RSS'
    end
  end
end

#models/source/rss.rb
class Source::RSS < Source::Base
  def get_content
    puts 'Got content from RSS'
  end
end

我想要这样的行为:

s = Source::Base.new(:url => 'http://stackoverflow.com/rss')
s.get_content #=> Got content from RSS

s2 = Source::Base.first # url is also ending rss
s2.get_content #=> Got content from RSS

3 个答案:

答案 0 :(得分:3)

至少有三种方法可以做到这一点:

1。使用Factory method

@Alejandro Babio的回答是这种模式的一个很好的例子。它有很少的缺点,但你必须记住始终使用工厂方法。如果第三方代码正在创建您的对象,这可能具有挑战性。

2。覆盖Source::Base.new

Ruby(包括其所有罪行)将允许您覆盖new

class Source::Base < ActiveRecord::Base
  def self.new(attributes)
    base = super
    return base if base.type == base.real_type
    base.becomes(base.real_type)
  end

  def real_type
    # type detection logic
  end
end

这是“神奇的”,带有所有超级酷且超级混乱的行李。

3。在转换方法

中换行becomes
class Source::Base < ActiveRecord::Base
  def become_real_type
    return self if self.type == self.real_type
    becomes(real_type)
  end

  def real_type
    # type detection logic
  end
end

thing = Source::Base.new(params).become_real_type

这与工厂方法非常相似,但它允许您在对象创建后进行转换,如果还有其他东西在创建对象,这可能会有所帮助。

答案 1 :(得分:2)

另一种选择是使用polymorphic association,您的类可能如下所示:

class Source < ActiveRecord::Base
  belongs_to :content, polymorphic: true
end

class RSS < ActiveRecord::Base
  has_one :source, as: :content
  validates :source, :url, presence: true
end

创建实例时,您需要创建源,然后创建并分配具体的content实例,因此:

s = Source.create
s.content = RSS.create url: exmaple.com

您可能希望accepts_nested_attributes_for更简单。

您的detect_type逻辑将位于控制器或service object中。它可以返回内容的正确类,例如return RSS if /(rss)$/ ~= self.url

使用这种方法,您可以要求Source.all includes: :content,当您为每个content实例加载Source时,Rails的多态性会将其实例化为正确的类型。

答案 2 :(得分:1)

如果我是你,我会添加一个返回正确实例的类方法。

class Source::Base < ActiveRecord::Base
  def self.new_by_url(params)
    type = if (/(rss)$/ ~= params[:url])
      'Source::RSS'
    end
    raise 'invalid type' unless type
    type.constantize.new(params)
  end
end

然后您将获得所需的行为:

s = Source::Base.new_by_url(:url => 'http://stackoverflow.com/rss')
s.get_content #=> Got content from RSS

s将是Source::RSS的实例。

注意:阅读完评论后:code使用klass.newnew是一种类方法。初始化后,您的对象已完成且为Source::Base,并且无法更改它。