ruby - create singleton with parameters?

落花浮王杯 提交于 2020-07-17 06:22:03

问题


I've seen how to define a class as being a singleton (how to create a singleton in ruby):

require 'singleton'

class Example
  include Singleton
end

But what if I want to give it some parameters for that single instance, meaning, the Example should always have certain properties initialized. For example, say I had a class whose sole purpose is to log to a file (this is just an example) but it requires a name of a file to log to before it can work.

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

How can I make MyLogger a singleton but make sure it gets a file_name?


回答1:


Singleton does not provide this functionality, but instead of using singleton you could write it by yourself

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

It should work, but I did not tested the code.

This code forces you to use MyLogger.instance <file_name> or at least at the first call if you know it will be first time calling.




回答2:


Here's another way to do it -- put the log file name in a class variable:

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

Now you can use it this way:

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

Subsequent calls to instance will return the same object with the path name unchanged, even if you later change the value of the class variable. A nice further touch would be to use another class variable to keep track of whether an instance has already been created, and have the file_name= method raise an exception in that case. You could also have initialize raise an exception if @@file_name has not yet been set.




回答3:


Here is an approach I used to solve a similar problem, which I wanted to share in case you or other people find it suitable:

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)

I've tested the code in 2.3, 2.4 and 2.5; earlier versions may of course exhibit divergent behavior.

This allows you to have a general parametrized Logger class, which can be used to create additional instances for testing or future alternative configurations, while defining MyLogger as a single instance of it following to Ruby's standardized Singleton pattern. You can split instance methods across them as you find appropriate.

Ruby's Singleton constructs the instance automatically when first needed, so the Logger#initialize parameters must be available on-demand in MyLogger.new, but you can of course pull the values from the environment or set them up as MyLogger class instance variables during configuration before the singleton instance is ever used, which is consistent with the singleton instance being effectively global.




回答4:


This was too long to put into a comment (e.g. stackoverflow said it was too long)

Ok so here's what I came up with:

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

This uses 'config_instance' to create and configure the singleton instance. It redefines the self.instance method once an instance is ready.

It also makes the 'new' class method private after creating the first instance.




回答5:


Simple singleton that doesn't depend on Singleton module

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

Example usage

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


来源:https://stackoverflow.com/questions/5259398/ruby-create-singleton-with-parameters

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!