Rails STI class 自动初始化

Rails STI class auto initialize

我正在尝试制作一个 STI 基础模型,它会自动更改为继承的 class,就像这样:

#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://whosebug.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

如果我是你,我会添加一个 class 方法来 returns 正确的实例。

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://whosebug.com/rss')
s.get_content #=> Got content from RSS

并且s将是Source::RSS的实例。

注意:在阅读您的评论后变为:其 code 使用 klass.new。而 new 是一个 class 方法。初始化后,你的对象就完成了,它是一个Source::Base,没有办法改变它。

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

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

这是 "magic",可以带来所有超酷和超混乱的行李。

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

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

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

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 内容的正确 class,例如return RSS if /(rss)$/ ~= self.url.


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