如何实现单例模式

36

我在Rails中有一个网站,想要设置全局设置。我的应用程序的一部分可以在特定事件发生时通过短信通知管理员。这是一个我想要通过全局设置配置的功能的示例。

所以我想我应该有一个Setting模型或类似的东西。它需要是一个模型,因为我想能够使用has_many:contacts来进行短信通知。

问题是数据库中只能有一个settings模型的帖子。所以我想使用Singleton模型,但那只能防止创建新对象,对吗?

我是否仍然需要为每个属性创建getter和setter方法,例如:

def self.attribute=(param)
  Model.first.attribute = param
end

def self.attribute
  Model.first.attribute
end

直接使用Model.attribute可能不是最佳实践,而总是创建一个实例并使用它会更好吗?

在这种情况下我该怎么做?

14个回答

70
我同意@user43685的观点,不同意@Derek P的观点 - 在数据库中保留站点范围数据而不是yaml文件有很多好处。例如:如果您有多个Web服务器,则您的设置将在所有Web服务器上可用;对设置的更改将是ACID;您不必花时间实现YAML包装器等等。
在Rails中,这很容易实现,您只需记住您的模型应该是数据库术语中的“单例”,而不是Ruby对象术语中的“单例”。
最简单的实现方法是:
1.添加一个新的模型,每个属性都有一个列
2.添加一个名为“singleton_guard”的特殊列,并验证它始终等于“0”,并将其标记为唯一(这将强制该表中只有一行)
3.向模型类添加一个静态帮助程序方法来加载单例行
因此,迁移应该像这样:
create_table :app_settings do |t|
  t.integer  :singleton_guard
  t.datetime :config_property1
  t.datetime :config_property2
  ...

  t.timestamps
end
add_index(:app_settings, :singleton_guard, :unique => true)

模型类应该长这样:

class AppSettings < ActiveRecord::Base
  # The "singleton_guard" column is a unique column which must always be set to '0'
  # This ensures that only one AppSettings row is created
  validates_inclusion_of :singleton_guard, :in => [0]

  def self.instance
    # there will be only one row, and its ID must be '1'
    begin
      find(1)
    rescue ActiveRecord::RecordNotFound
      # slight race condition here, but it will only happen once
      row = AppSettings.new
      row.singleton_guard = 0
      row.save!
      row
    end
  end
end

在Rails >= 3.2.1中,您可以将"instance" getter的主体替换为对"first_or_create!"的调用,如下所示:

def self.instance
  first_or_create!(singleton_guard: 0)
end

5
如果有人想省几分钟时间,我建议不要使用名为“Config”的模型:http://oldwiki.rubyonrails.org/rails/pages/ReservedWords - user657199
谢谢,@Ricky,我已经重命名了模型。 - Rich
3
那些保留字总是会让你措手不及! - user657199
为什么不这样做:class AppSettings < ActiveRecord::Base def self.instance AppSettings.first_or_create!(...) end end - BvuRVKyUVlViVIc7
谢谢,@Lichtamberg,我已经将它添加到答案中。我认为在我写这篇文章的时候它还不存在;它是在2012年1月的Rails 3.2.1中添加的。 - Rich
显示剩余2条评论

25

我不同意普遍观点 - 从数据库中读取属性并没有什么问题。你可以读取数据库的值并进行冻结,但是有更灵活的替代方案来进行简单的冻结。

YAML和数据库有何不同?..相同的方式 - 外部到应用程序代码持久设置。

使用数据库方法的好处在于它可以以更安全的方式在运行时更改(不直接打开和覆盖文件)。另一个好处是,如果正确实现,它可以在集群节点之间共享网络。

然而,问题仍然是如何使用ActiveRecord来实现这样的设置的正确方式。


1
我正在阅读这篇文章,因为我面临着类似的挑战——但我同意使用 YAML 文件是一个糟糕的选择。我的网站将有多个具有管理员权限的用户,他们可以编辑网站设置,而我不能指望他们去编辑配置文件。 - Jeff

14

您还可以按照以下方式强制最多只保留一条记录:

class AppConfig < ActiveRecord::Base

  before_create :confirm_singularity

  private

  def confirm_singularity
    raise Exception.new("There can be only one.") if AppConfig.count > 0
  end

end

这将覆盖ActiveRecord方法,因此如果您尝试在已存在一个实例的情况下创建该类的新实例,它将会崩溃。

然后,您可以继续定义仅作用于该记录的类方法:

class AppConfig < ActiveRecord::Base

  attr_accessible :some_boolean
  before_create :confirm_singularity

  def self.some_boolean?
    settings.some_boolean
  end

  private

  def confirm_singularity
    raise Exception.new("There can be only one.") if AppConfig.count > 0
  end

  def self.settings
    first
  end

end

引用 ShopGlobalSetting 类是有意为之吗?AppConfig 计数实际上会显示这个类有一个实例吗? - Chris Bosco
不对,应该是 AppConfig。发现得好,我会改的。而且,他的计数调用应该能正常工作。 - hoffm

8
我不确定是否有必要为这样一个基本需求使用数据库/ActiveRecord/Model的开销。假设这些数据相对静态,并且不需要实时计算(包括数据库查询)。
话虽如此,我建议您定义一个YAML文件,其中包含您网站的全局设置,并定义一个初始化程序文件,该文件将这些设置加载到常量中。这样,您就不会有太多不必要的移动部件了。
这些数据可以只保存在内存中,这样可以避免复杂度的增加。常量可以在任何地方使用,它们不需要被初始化或实例化。如果您确实需要使用类作为单例模式,建议执行以下两个步骤:
1.取消初始化/新方法 2.仅定义self.*方法,以便您无法维护状态

12
使用 .yml 文件缺乏灵活性,不利于用户(而不是网站作者)更新设置。 - Undistraction
你知道Rails中的动态角色和权限吗?它不能使用YAML静态文件。通常有很多属性设置会发生变化。 - Johan

6
我知道这是一个旧的帖子,但我也需要同样的东西,并发现有一个gem可以做到这一点:acts_as_singleton
安装说明适用于Rails 2,但在Rails 3中也很好用。

3
很可能你不需要一个单例模式。遗憾的是,模式热潮中出现的最糟糕的设计习惯之一也是最常被采用的。我认为这是因为它看起来简单,但我扯远了。如果他们称之为“静态全局”模式,我相信人们会更加谨慎地使用它。
我建议使用包装类,并在其中使用要用于单例的私有静态实例。这样做不会像使用单例那样在整个代码中引入紧密耦合。
有些人将其称为单态模式。我倾向于将其视为策略/代理概念的另一种变体,因为您可以通过实现不同的接口来公开/隐藏功能以获得更大的灵活性。

2
我将对之前的答案进行一些补充:
  • 不需要为唯一索引添加单独的字段,我们可以在id字段上设置约束。由于在Rails应用程序中我们应该已经有了id字段,这是一个很好的权衡
  • 不需要在模型上进行特殊验证和显式ID分配,只需要在列上设置默认值即可

如果我们应用这些修改,解决方案就变得非常容易:

# migration
create_table :settings, id: false do |t|
  t.integer :id, null: false, primary_key: true, default: 1, index: {unique: true}
  t.integer :setting1
  t.integer :setting2
  ...
end

# model
class Settings < ApplicationRecord
  def self.instance
    first_or_create!(...)
  end  
end

为什么不使用 def self.find(*args); first_or_create!; end 而不是 self.instance - Joshua Muheim
1
@JoshuaMuheim 由你决定。instance 只是单例方法的一个通用名称,仅此而已。 - ka8725
默认值保证了create方法将使用id: 1执行,但实际上您的表可以包含尽可能多的值(直到超出范围),因此连续调用create(id: 1)、create(id: 2)将导致2条记录。 - Nick Roz
唯一索引在主键上以及非空是多余的。 - Nick Roz

2

简单:

class AppSettings < ActiveRecord::Base 
  before_create do
    self.errors.add(:base, "already one setting object existing") and return false if AppSettings.exists?      
  end

  def self.instance
    AppSettings.first_or_create!(...) 
  end 
end

1
class Constant < ActiveRecord::Base
  after_initialize :readonly!

  def self.const_missing(name)
    first[name.to_s.downcase]
  end
end

Constant::FIELD_NAME


考虑一起覆盖 const_defined 和 const_missing 吗?https://apidock.com/ruby/Module/const_defined%3F。我不确定,但它可能很有用。 - gayavat

1

使用has_many :contacts并不意味着你需要一个模型。 has_many做了一些魔法,但最终它只是添加了一些具有指定契约的方法。没有理由不能实现这些方法(或者你需要的某个子集)来使你的模型表现得像它有has_many :contacts,但实际上并不使用ActiveRecord模型(或根本不使用Contact模型)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接