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:
1class Singleton
2 class << self
3 def instance
4 @instance ||= new
5 end
6
7 private :new
8 end
9end
10
11# access the instance
12Singleton.instance # => #<Singleton:0x007fd70c889880>
13
14# cannot instantiate
15Singleton.new # ~> -:12:in `<main>': 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.
1class Configuration
2 attr_writer :credentials_file
3
4 def credentials_file
5 @credentials_file || raise("credentials file not set")
6 end
7end
8
9MyConfig = Configuration.new # Somewhere in your app
10
11describe Configuration do
12 let(:config) { Configuration.new }
13 describe 'credentials_file' do
14 specify 'it can be set' do
15 config.credentials_file = 'abc'
16 config.credentials_file.should == 'abc'
17 end
18
19 specify 'raises an error if accessed before being initialized' do
20 expect { config.credentials_file }.to raise_error 'credentials file not set'
21 end
22 end
23end
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.
1class Configuration
2 def self.instance
3 @instance ||= new
4 end
5
6 attr_writer :credentials_file
7
8 def credentials_file
9 @credentials_file || raise("credentials file not set")
10 end
11end
12
13# to access the singleton
14Configuration.instance # => #<Configuration:0x007fe2dc08a7e8>
15
16describe Configuration do
17 let(:config) { Configuration.new } # test against fresh instance
18
19 specify '.instance always refers to the same instance' do
20 Configuration.instance.should be_a_kind_of Configuration
21 Configuration.instance.should equal Configuration.instance
22 end
23
24 describe 'credentials_file' do
25 specify 'it can be set' do
26 config.credentials_file = 'abc'
27 config.credentials_file.should == 'abc'
28 end
29
30 specify 'raises an error if accessed before being initialized' do
31 expect { config.credentials_file }.to raise_error 'credentials file not set'
32 end
33 end
34end
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.
1class Configuration
2 def self.credentials_file=(credentials_file)
3 @credentials_file = credentials_file
4 end
5
6 def self.credentials_file
7 @credentials_file || raise("credentials file not set")
8 end
9end
10
11describe Configuration do
12 let(:config) { Class.new Configuration }
13 describe 'credentials_file' do
14 specify 'it can be set' do
15 config.credentials_file = 'abc'
16 config.credentials_file.should == 'abc'
17 end
18
19 specify 'raises an error if accessed before being initialized' do
20 expect { config.credentials_file }.to raise_error 'credentials file not set'
21 end
22 end
23end
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).
1class Configuration
2 def self.reset
3 @credentials_file = nil
4 end
5
6 class << self
7 attr_writer :credentials_file
8 end
9
10 def self.credentials_file
11 @credentials_file || raise("credentials file not set")
12 end
13end
14
15RSpec.configure do |config|
16 config.before { Configuration.reset }
17end
18
19describe Configuration do
20 describe 'credentials_file' do
21 specify 'it can be set' do
22 Configuration.credentials_file = 'abc'
23 Configuration.credentials_file.should == 'abc'
24 end
25
26 specify 'raises an error if accessed before being initialized' do
27 expect { Configuration.credentials_file }.to raise_error 'credentials file not set'
28 end
29 end
30end
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.
1class Configuration
2 def self.credentials_file=(credentials_file)
3 @credentials_file = credentials_file
4 end
5
6 def self.credentials_file
7 @credentials_file || raise("credentials file not set")
8 end
9end
10
11describe Configuration do
12 let(:configuration) { Configuration.clone }
13
14 describe 'credentials_file' do
15 specify 'it can be set' do
16 configuration.credentials_file = 'abc'
17 configuration.credentials_file.should == 'abc'
18 end
19
20 specify 'raises an error if accessed before being initialized' do
21 expect { configuration.credentials_file }.to raise_error 'credentials file not set'
22 end
23 end
24end
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.
1module ConfigurationBehaviour
2 attr_writer :credentials_file
3 def credentials_file
4 @credentials_file || raise("credentials file not set")
5 end
6end
7
8# Somewhere in your app
9class Configuration
10 extend ConfigurationBehaviour
11end
12
13describe Configuration do
14 let(:configuration) { Class.new.extend ConfigurationBehaviour }
15
16 describe 'credentials_file' do
17 specify 'it can be set' do
18 configuration.credentials_file = 'abc'
19 configuration.credentials_file.should == 'abc'
20 end
21
22 specify 'raises an error if accessed before being initialized' do
23 expect { configuration.credentials_file }.to raise_error 'credentials file not set'
24 end
25 end
26end
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.