如何将单例模式与接受初始化参数的类相结合创建一个类?

24

我知道如何将一个类定义为单例 (如何在Ruby中创建单例):

require 'singleton'
 
class Example
  include Singleton
end

但是如果我想在初始化单个实例时向#new传递一些参数怎么办?示例应始终初始化某些属性。例如,假设我有一个唯一目的是将日志记录到文件的类,但它需要一个文件名才能开始工作。

class MyLogger
  def initialize(file_name)
    @file_name = file_name
  end
end

我如何将MyLogger设为单例,但又确保它获取到一个文件名?


2
如果你想传递参数,你确定要使用单例模式吗? - Andrew Grimm
4
是的。我相信有些情况下单例是有意义的,但应该对其进行一些初始配置。 - codecraig
+1 因为这是一个好问题。Singleton 使 #new 成为私有方法,即使在 Ruby 3.1.2 中,文档也没有解释如何在不使用元编程的情况下公开它,尽管有一些模糊的方法可能会有用。这里可能有比这里更棒的答案(包括我的),但这是一个很棒的问题,因为它具有挑战性,而不是不寻常的边缘情况。 - Todd A. Jacobs
6个回答

16

这里有另一种方法可以做到这一点--将日志文件名放入类变量中:

require 'singleton'
class MyLogger
  include Singleton
  @@file_name = ""
  def self.file_name= fn
    @@file_name = fn
  end
  def initialize
    @file_name = @@file_name
  end
end

现在你可以这样使用它:

MyLogger.file_name = "path/to/log/file"
log = MyLogger.instance  # => #<MyLogger:0x000.... @file_name="path/to/log/file">

后续对 instance 的调用将返回相同的对象,并且路径名称不变,即使您稍后更改类变量的值。另一个很好的点是使用另一个类变量来跟踪是否已创建实例,并在这种情况下让 file_name= 方法引发异常。您还可以让 initialize 在尚未设置 @@file_name 的情况下引发异常。


4
既然 @@file_name 是单例(只有一个实例),为什么要将其复制到 @file_name 中,而不直接使用 @@file_name 呢? - Gavriel

4

单例模式并不提供这种功能,但是你可以自己编写代码来实现。

class MyLogger
  @@singleton__instance__ = nil
  @@singleton__mutex__    = Mutex.new

  def self.instance(file_name)
    return @@singleton__instance__ if @@singleton__instance__

    @@singleton__mutex__.synchronize do
      return @@singleton__instance__ if @@singleton__instance__

      @@singleton__instance__ = new(file_name)
    end
    @@singleton__instance__
  end

  private

  def initialize(file_name)
    @file_name = file_name
  end
  private_class_method :new
end

这应该可以正常工作,但我没有测试过这段代码。

这段代码强制你使用MyLogger.instance <file_name>或者至少在第一次调用时使用(如果你知道它将是第一次调用)。


所以我认为在创建@@__singleton_instance__之后,我必须重写self.new,否则你仍然可以执行MyLogger.new。 - codecraig
好的,这是我想出来的: - codecraig
1
为了更加通用,可以在方法self.instance中使用splat运算符设置*params,例如(def self.instance *params),并且在调用new时也使用splat运算符,如new(*params) - rplaurindo

3

这里是我用来解决类似问题的一种方法,我想分享出来以便您或其他人找到合适的解决方案:

require 'singleton'

class Logger
  attr_reader :file_name

  def initialize file_name
    @file_name = file_name
  end
end


class MyLogger < Logger
  include Singleton

  def self.new
    super "path/to/file.log"
  end

  # You want to make {.new} private to maintain the {Singleton} approach;
  # otherwise other instances of {MyLogger} can be easily constructed.
  private_class_method :new
end

p MyLogger.instance.file_name
# => "path/to/file.log"

MyLogger.new "some/other/path"
# => ...private method `new' called for MyLogger:Class (NoMethodError)

我已经在 2.32.42.5 版本上测试了代码; 更早的版本可能会有不同的行为。

这使您可以拥有一个通用的带参数化的 Logger 类,可以用于创建额外的实例进行测试或将来的其他配置,同时将 MyLogger 定义为它的单一实例遵循 Ruby 的标准 Singleton 模式。您可以根据需要将实例方法分配给它们。

Ruby 的 Singleton 在第一次需要时自动构建实例,因此在 MyLogger.new 中必须按需提供 Logger#initialize 参数,但您当然可以从环境中提取值或在使用单例实例之前在配置期间设置它们作为 MyLogger 类实例变量,这与单例实例实际上是全局的是一致的。


1

这段内容太长了,无法放在评论中(例如stackoverflow说它太长了)

好的,这是我想出来的:

class MyLogger
  @@singleton__instance__ = nil
  @@singleton__mutex__ = Mutex.new
  def self.config_instance file_name
    return @@singleton__instance__ if @@singleton__instance__
    @@singleton__mutex__.synchronize {
      return @@singleton__instance__ if @@singleton__instance__
      @@singleton__instance__ = new(file_name)
      def self.instance
        @@singleton__instance__
      end
      private_class_method :new
    }
    @@singleton__instance__
  end
  def self.instance
    raise "must call MyLogger.config_instance at least once"
  end
  private
  def initialize file_name
    @file_name = file_name
  end
end

这个程序使用 'config_instance' 来创建和配置单例实例。一旦实例准备好,它会重新定义 self.instance 方法。

在创建第一个实例后,它还会将 'new' 类方法设置为私有。


在我的回答中修正,您可以在类级别上设置私有变量new,但new仍然对于def self.instance可见。 - mpapis

1

简单的单例模式,不依赖于Singleton模块

class MyLogger
  def self.instance(filepath = File.join('some', 'default', 'path'))
    @@instance ||= new(filepath).send(:configure)
  end

  def initialize(filepath)
    @filepath = filepath
  end
  private_class_method :new

  def info(msg)
    puts msg
  end

  private

  def configure
    # do stuff
    self
  end
end

示例用法
logger_a = MyLogger.instance
# => #<MyLogger:0x007f8ec4833060 @filepath="some/default/path">

logger_b = MyLogger.instance
# => #<MyLogger:0x007f8ec4833060 @filepath="some/default/path">

logger_a.info logger_a.object_id
# 70125579507760
# => nil

logger_b.info logger_b.object_id
# 70125579507760
# => nil

logger_c = MyLogger.new('file/path')
# NoMethodError: private method `new' called for MyLogger:Class

0

使用访问器

虽然我还没有找到一个真正优雅的解决方案来按照你想要的方式初始化单例,但使用访问器来实现这一点相当简单。例如,在Ruby 3.1.2中:

require 'singleton'

class Example
  include Singleton
  attr_accessor :file_name
end

#=> [:file_name, :file_name=]

如果实例变量自动转换为 nil,则可以使用访问器来填充@file_name,如果它尚未包含真值:

Example.instance.file_name ||= "/tmp/foo"
#=> "/tmp/foo"

Example.instance.file_name
#=> "/tmp/foo"

Example.instance
#=> #<Example:0x0000000108da72b8 @file_name="/tmp/foo">

在设置值后删除可写访问器

显然,您也可以重新分配变量,因为您有一个可写访问器。如果您不想在第一次设置实例变量后允许单例类这样做,请在其为真时取消定义attr_writer方法:

# @return [Class<Example>, nil] class if method removed; otherwise nil
Example.instance.singleton_class.undef_method(:file_name=) if
   Example.instance.file_name && Example.instance.respond_to?(:file_name=)

简化版(有注意事项)

你可以像这样一次性完成所有操作:

require 'singleton'

class Example
  include Singleton
  attr_accessor :file_name
end

Example.instance.file_name ||= "/tmp/foo"
Example.instance.singleton_class.undef_method(:file_name=)
Example.instance
#=> #<Example:0x0000000108da72b8 @file_name="/tmp/foo">

但是,如果您事先不知道实例变量是否已设置,则可能更喜欢更冗长的方法。您的使用情况肯定会有所不同。


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