What is a Singleton Object?
The GoF identifies a singleton as a way to "ensure a class only has one instance, and provide a global point of access to it".
The implementations I provide below will be lax on both of these points.
Here is a minimal example using the GoF's implementation:
class Singleton
class << self
def instance
@instance ||= new
end
private :new
end
end
# access the instance
Singleton.instance # => #
# cannot instantiate
Singleton.new # ~> -:12:in `': private method `new' called for Singleton:Class (NoMethodError)
Note that Ruby also provides a singleton helper module in the stdlib. I dislike it because it's true to the GoF's implementation and singletons == meh.
Singletons == meh
It sounds like a great idea: you have one logger, and everything logs through it. But can you really say that you shouldn't be able to log somewhere else or in some other way if you wanted to? What if you later decide you want database queries to log in one place and http requests to log in another? It's easy to think you'll only ever have one of something, but I can't think of any example where this doesn't risk breaking down.
You will experience difficulty testing these because if they retain state (e.g. a logger with a file_name) then you will need to figure out how to reset that state between tests. If you wind up needing to reset them or change their state for your tests, then can you really say that you won't ever need this in your production code? Can you really say there will only ever be one Rails application, one database, one print spool, and one logger?
The second reason singletons == meh is the global point of access. That's a hard dependency. Hard dependencies are convenient when you have no infrastructure to support dependency injection, but they couple your code. This causes the code depending on the singleton to have direct access to the world it lives in, giving up its modularity and its independence from its environment. This makes it difficult to use the code in novel way. It effectively turns the singleton into a global variable.
What to do about it?
I've provided a number of implementations which show how to can get around the singleness of the object.
To get around the problems of a global point of access, opt for some form of dependency injection rather than accessing these objects directly. You can pass the singleton in when initializing, or use one of the many gems that address this problem.
Here are six approaches and example tests demonstrating their use:
1. Make the singleton an instance of some other class
This approach frees that class from having to manage the single point of access to its instances. Then you can test the other class in isolation, and don't need to test your singleton explicitly. This one is my favourite, which is why it is first.
class Configuration
attr_writer :credentials_file
def credentials_file
@credentials_file || raise("credentials file not set")
end
end
MyConfig = Configuration.new # Somewhere in your app
describe Configuration do
let(:config) { Configuration.new }
describe 'credentials_file' do
specify 'it can be set' do
config.credentials_file = 'abc'
config.credentials_file.should == 'abc'
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
2. Provide an instance of the class, but allow the class to be instantiated
This is in line with the way singletons are traditionally presented, except it does not prevent instantiation. Any time you want to refer to the singleton, you talk to the singleton instance, but you can test against other instances.
class Configuration
def self.instance
@instance ||= new
end
attr_writer :credentials_file
def credentials_file
@credentials_file || raise("credentials file not set")
end
end
# to access the singleton
Configuration.instance # => #
describe Configuration do
let(:config) { Configuration.new } # test against fresh instance
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' do
config.credentials_file = 'abc'
config.credentials_file.should == 'abc'
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
3. Subclass the singleton in tests
This allows you to change the state of the subclass without effecting the state of the singleton. This is useful when the object is a class, a pattern which is best to avoid, but is not uncommon.
class Configuration
def self.credentials_file=(credentials_file)
@credentials_file = credentials_file
end
def self.credentials_file
@credentials_file || raise("credentials file not set")
end
end
describe Configuration do
let(:config) { Class.new Configuration }
describe 'credentials_file' do
specify 'it can be set' do
config.credentials_file = 'abc'
config.credentials_file.should == 'abc'
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
4. Ensure that there is a way to reset the singleton.
Make sure any changes to the singlenton's state can be undone. (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 Configuration do
describe 'credentials_file' do
specify 'it can be set' do
Configuration.credentials_file = 'abc'
Configuration.credentials_file.should == 'abc'
end
specify 'raises an error if accessed before being initialized' does
expect { Configuration.credentials_file }.to raise_error 'credentials file not set'
end
end
end
5. Clone the class instead of testing it directly.
This came out of a gist I made. After cloning the class, you edit the clone instead of the original class. This solution works around some of the difficulties of singletons rather than addressing their problems directly. It has caveats which could cause bugs. For example if the class has instance variables, you need to worry about whether what happens to the clone could affect variables that the cloned class points to. There is no real deep copy in Ruby.
class Configuration
def self.credentials_file=(credentials_file)
@credentials_file = credentials_file
end
def self.credentials_file
@credentials_file || raise("credentials file not set")
end
end
describe Configuration do
let(:configuration) { Configuration.clone }
describe 'credentials_file' do
specify 'it can be set' do
configuration.credentials_file = 'abc'
configuration.credentials_file.should == 'abc'
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
6. Develop the behaviour in modules, then extend that onto the singleton.
You would probably need to look into the self.included
and self.extended
methods if you needed to initialize some variables on the object. This creates more code and disassociates the objects from their methods which may force you to traverse several files before you find the code that defines the method you're interested in. It's also an extra stop along the call chain. This gist has a slightly more involved example.
module ConfigurationBehaviour
attr_writer :credentials_file
def credentials_file
@credentials_file || raise("credentials file not set")
end
end
# Somewhere in your app
class Configuration
extend ConfigurationBehaviour
end
describe Configuration do
let(:configuration) { Class.new.extend ConfigurationBehaviour }
describe 'credentials_file' do
specify 'it can be set' do
configuration.credentials_file = 'abc'
configuration.credentials_file.should == 'abc'
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
The End <3
I think my preferred solution would be the ability to instantiate singleton classes. As this doesn't exist, hopefully one of these solutions will meet your needs. I've used several of them and recommend the first solution, but there are times when the others have been more appropriate. Choose the one that works for you.