在 Ruby 中重置单例实例 [英] Resetting a singleton instance in Ruby

查看:26
本文介绍了在 Ruby 中重置单例实例的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

如何在 Ruby 中重置单例对象?我知道人们永远不想在真实代码中这样做,但是单元测试呢?

How do I reset a singleton object in Ruby? I know that one'd never want to do this in real code but what about unit tests?

这是我在 RSpec 测试中尝试做的 -

Here's what I am trying to do in an RSpec test -

describe MySingleton, "#not_initialised" do
  it "raises an exception" do
    expect {MySingleton.get_something}.to raise_error(RuntimeError)
  end
end

它失败了,因为我之前的测试之一初始化了单例对象.我已经尝试从 this 链接中遵循 Ian White 的建议本质上是猴子修补 Singleton 以提供 reset_instance 方法,但我得到了一个未定义的方法reset_instance"异常.

It fails because one of my previous tests initialises the singleton object. I have tried following Ian White's advice from this link which essentially monkey patches Singleton to provide a reset_instance method but I get an undefined method 'reset_instance' exception.

require 'singleton'

class <<Singleton
  def included_with_reset(klass)
    included_without_reset(klass)
    class <<klass
      def reset_instance
        Singleton.send :__init__, self
        self
      end
    end
  end
  alias_method :included_without_reset, :included
  alias_method :included, :included_with_reset
end

describe MySingleton, "#not_initialised" do
  it "raises an exception" do
    MySingleton.reset_instance
    expect {MySingleton.get_something}.to raise_error(RuntimeError)
  end
end

在 Ruby 中执行此操作最惯用的方法是什么?

What is the most idiomatic way to do this in Ruby?

推荐答案

棘手的问题,单身人士很粗糙.部分是因为您正在展示(如何重置它),部分是因为他们做出的假设有可能会在以后咬你(例如大多数 Rails).

Tough question, singletons are rough. In part for the reason that you're showing (how to reset it), and in part because they make assumptions that have a tendency to bite you later (e.g. most of Rails).

您可以做几件事情,但它们充其量都没问题".最好的解决方案是找到一种摆脱单身的方法.我知道,这是手动的,因为没有可以应用的公式或算法,它消除了很多便利,但如果你能做到,这通常是值得的.

There are a couple of things you can do, they're all "okay" at best. The best solution is to find a way to get rid of singletons. This is hand-wavy, I know, because there isn't a formula or algorithm you can apply, and it removes a lot of convenience, but if you can do it, it's often worthwhile.

如果做不到,至少尝试注入单例而不是直接访问它.现在测试可能很难,但想象一下必须在运行时处理这样的问题.为此,您需要内置基础设施来处理它.

If you can't do it, at least try to inject the singleton rather than accessing it directly. Testing might be hard right now, but imagine having to deal with issues like this at runtime. For that, you'd need infrastructure built in to handle it.

以下是我想到的六种方法.

Here are six approaches I have thought of.

提供类的实例,但允许类被实例化.这是最符合单例传统呈现方式的.基本上任何时候你想引用单例,你都可以与单例实例对话,但你可以针对其他实例进行测试.stdlib 中有一个模块可以帮助解决这个问题,但它使 .new 成为私有的,所以如果你想使用它,你必须使用类似 let(:config) { Configuration.send :new } 来测试它.

Provide an instance of the class, but allow the class to be instantiated. This is the most in line with the way singletons are traditionally presented. Basically any time you want to refer to the singleton, you talk to the singleton instance, but you can test against other instances. There's a module in the stdlib to help with this, but it makes .new private, so if you want to use it you'd have to use something like let(:config) { Configuration.send :new } to test it.

class Configuration
  def self.instance
    @instance ||= new
  end

  attr_writer :credentials_file

  def credentials_file
    @credentials_file || raise("credentials file not set")
  end
end

describe Config do
  let(:config) { Configuration.new }

  specify '.instance always refers to the same instance' do
    Configuration.instance.should be_a_kind_of Configuration
    Configuration.instance.should equal Configuration.instance
  end

  describe 'credentials_file' do  
    specify 'it can be set/reset' do
      config.credentials_file = 'abc'
      config.credentials_file.should == 'abc'
      config.credentials_file = 'def'
      config.credentials_file.should == 'def'
    end

    specify 'raises an error if accessed before being initialized' do
      expect { config.credentials_file }.to raise_error 'credentials file not set'
    end
  end
end

然后在任何你想访问它的地方,使用 Configuration.instance

Then anywhere you want to access it, use Configuration.instance

使单例成为其他类的实例.然后你可以单独测试另一个类,不需要显式测试你的单例.

Making the singleton an instance of some other class. Then you can test the other class in isolation, and don't need to test your singleton explicitly.

class Counter
  attr_accessor :count

  def initialize
    @count = 0
  end

  def count!
    @count += 1
  end
end

describe Counter do
  let(:counter) { Counter.new }
  it 'starts at zero' do
    counter.count.should be_zero
  end

  it 'increments when counted' do
    counter.count!
    counter.count.should == 1
  end
end

然后在您的应用程序中的某个地方:

Then in your app somewhere:

MyCounter = Counter.new

<小时>

您可以确保永远不要编辑主类,然后只需将其子类化即可用于您的测试:

class Configuration
  class << self
    attr_writer :credentials_file
  end

  def self.credentials_file
    @credentials_file || raise("credentials file not set")
  end
end

describe Config do
  let(:config) { Class.new Configuration }
  describe 'credentials_file' do  
    specify 'it can be set/reset' do
      config.credentials_file = 'abc'
      config.credentials_file.should == 'abc'
      config.credentials_file = 'def'
      config.credentials_file.should == 'def'
    end

    specify 'raises an error if accessed before being initialized' do
      expect { config.credentials_file }.to raise_error 'credentials file not set'
    end
  end
end

然后在您的应用程序中的某个地方:

Then in your app somewhere:

MyConfig = Class.new Configuration

<小时>

确保有一种方法可以重置单例.或者更一般地说,撤消您所做的任何事情.(例如,如果您可以使用单例注册某个对象,那么您需要能够取消注册它,例如,在 Rails 中,当您子类化 Railtie 时,它会将其记录在一个数组中,但是您可以访问数组并从中删除项目).


Ensure that there is a way to reset the singleton. Or more generally, undo anything you do. (e.g. if you can register some object with the singleton, then you need to be able to unregister it, in Rails, for example, when you subclass Railtie, it records that in an array, but you can access the array and delete the item from it).

class Configuration
  def self.reset
    @credentials_file = nil
  end

  class << self
    attr_writer :credentials_file
  end

  def self.credentials_file
    @credentials_file || raise("credentials file not set")
  end
end

RSpec.configure do |config|
  config.before { Configuration.reset }
end

describe Config do
  describe 'credentials_file' do  
    specify 'it can be set/reset' do
      Configuration.credentials_file = 'abc'
      Configuration.credentials_file.should == 'abc'
      Configuration.credentials_file = 'def'
      Configuration.credentials_file.should == 'def'
    end

    specify 'raises an error if accessed before being initialized' do
      expect { Configuration.credentials_file }.to raise_error 'credentials file not set'
    end
  end
end

<小时>

克隆类而不是直接测试它.这是我制作的 gist,基本上你编辑的是克隆而不是真正的类.


Clone the class instead of testing it directly. This came out of a gist I made, basically you edit the clone instead of the real class.

class Configuration  
  class << self
    attr_writer :credentials_file
  end

  def self.credentials_file
    @credentials_file || raise("credentials file not set")
  end
end

describe Config do
  let(:configuration) { Configuration.clone }

  describe 'credentials_file' do  
    specify 'it can be set/reset' do
      configuration.credentials_file = 'abc'
      configuration.credentials_file.should == 'abc'
      configuration.credentials_file = 'def'
      configuration.credentials_file.should == 'def'
    end

    specify 'raises an error if accessed before being initialized' do
      expect { configuration.credentials_file }.to raise_error 'credentials file not set'
    end
  end
end

<小时>

开发模块中的行为,然后将其扩展到单例.这里是一个稍微复杂的例子.可能您必须查看 self.includedself.extended 方法,如果你需要初始化对象上的一些变量.


Develop the behaviour in modules, then extend that onto singleton. Here is a slightly more involved example. Probably you'd have to look into the self.included and self.extended methods if you needed to initialize some variables on the object.

module ConfigurationBehaviour
  attr_writer :credentials_file
  def credentials_file
    @credentials_file || raise("credentials file not set")
  end
end

describe Config do
  let(:configuration) { Class.new { extend ConfigurationBehaviour } }

  describe 'credentials_file' do  
    specify 'it can be set/reset' do
      configuration.credentials_file = 'abc'
      configuration.credentials_file.should == 'abc'
      configuration.credentials_file = 'def'
      configuration.credentials_file.should == 'def'
    end

    specify 'raises an error if accessed before being initialized' do
      expect { configuration.credentials_file }.to raise_error 'credentials file not set'
    end
  end
end

然后在您的应用程序中的某个地方:

Then in your app somewhere:

class Configuration  
  extend ConfigurationBehaviour
end

这篇关于在 Ruby 中重置单例实例的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆