What is a Singleton?
A singleton is a creational design pattern in which a class is responsible for the creation and management of its sole instance and it provides a way for the instance to be accessed from the global namespace.
For more information on the singleton pattern I highly recommend Source Making’s explanation.
The Singleton and Me
Currently I use a custom logging mechanism that is implemented using the singleton pattern and a more basic version of this will be what I use in the code example. The logger needs to behave differently based on the environment in which it is executed (development, testing, or production). Loggers in general are typically designed as a singleton. Subclassing a singleton is not different logistically as it would be with any other class. The trick is to make sure that proper instance is available when requested. For this purpose I used a registry of singletons.
Let’s Get to the Code
The code below is written in ruby and is one of the many ways to create a singleton along with its registry in this language. It is composed of the Logger class and two of its children, the ProductionLogger and the DevelopmentLogger.
The Logger Class
class Logger
@@instance = new
def initialize *args, &block
populate_registry
end
def self.instance
Logger.singleton_registry_look_up ENV['DEVELOPMENT_ENVIRONMENT']
end
#using a mutex for thread safety
def self.mutex
@mutex ||= Mutex.new
end
def self.registry
@registry ||= {}
end
#force all children to implement log so there is a common interface
def log information
raise "Must Implement Log Method"
end
def close
@log.nil? ? (raise "Must Declare Log Variable as @log") : @log.close
end
def self.singleton_registry_look_up singleton_name
self.registry[singleton_name.to_sym]
end
private
def register singleton_name, logger_type
self.class.registry[singleton_name.to_sym] = logger_type
end
#major drawback of using static variables is that all subclasses have to be
#instantiated so that they can be registered
def populate_registry
["DevelopmentLogger", "ProductionLogger"].each do |logger_type|
register logger_type, Object.const_get(logger_type).send(:new)
end
end
def logger_mutex
self.class.mutex.synchronize { yield }
end
private_class_method :new
end
What Makes Logger Class a Singleton
Making the Logger.new method private keeps new Logger instances from being created, the only Logger instance belongs in the @@instance class variable.
Setting Up the Registry of Singletons
When the new method is called and assigned to the @@instance variable it performs new’s normal duties which involves calling the private method initialize . The initialize method on the Logger class starts a chain of events that create the registry and then populates it.
The registry itself, a hash, holds the name of the classes (keys) and also an instance of the class (values). The point of the registry of singletons is to provide the correct instance when an instance is requested. This is done via the Logger.singleton_registry_look_up which takes the string name of the class and returns an instance. I prefer to read the string from the environment but there are other ways to do this as well.
The DevelopmentLogger Class
class DevelopmentLogger < Logger
def initialize *args, &block
@log = File.open('log/development.log', 'a')
end
def log information
#development log does not need to redact any information
logger_mutex { @log.puts information }
end
end
This class inherits from the Logger class and overwrites the initialize method to assign the proper log file to the class variable called @log . It also implements the log method which is abstracted in the parent. The log method literally just appends the argument to the file assigned to the @log variable.
The ProductionLogger Class
class ProductionLogger < Logger
def initialize *args, &block
@log = File.open('log/production.log', 'a')
end
def log information
#the actual password should be redacted in production log
logger_mutex { @log.puts information.gsub(/(?<=password:\s).*/im, "FILTERED") }
end
end
This class also overwrites the initialize method and opens the correct log for production. The ProductionLogger’s log implementation filters out any potential passwords that meet the regular expression from the log file.
Using the Logger Class
#inject into the application load './logger.rb' #access the instance LOGGER = Logger.instance #write to log LOGGER.log "Hello Log!"
Using load to inject the Logger into your application executes the @@instance assignment. Assigning the Logger.instance to a constant ensure reassignment won’t happen later.
The Singleton and Its Registry
There are plenty of reasons people do not use the singleton design pattern. Furthermore using a registry can cause further implementation problems. One implementation choice I struggled with was when and how to register subclasses in the registry. I came to the conclusion that it varies on language and needs. Even though the singleton does have drawbacks in my opinion there are valid use cases for them. For instance a logger being implemented as a singleton alleviates the probability of several different objects trying to access the same resource (usually a file) at the same time. If you throw in some thread safety you can be confident the log file has optimal integrity.
Find the Code
* For the sake of this tutorial I did not use ruby’s multiton or singleton modules, however the same effect could have been achieved with them.