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.