viphat / til

Today I Learned
http://notes.viphat.work
0 stars 1 forks source link

[Ruby] - [Design Patterns in Ruby] - 12. Singleton Pattern #232

Open viphat opened 6 years ago

viphat commented 6 years ago

In this Chapter, we will look at why we you might need a singleton, how you would go about building singletons and singleton-like things in Ruby, why singletons cause trouble and what you can do to ease some of this pain.

One Object, Global Access

The Motivation behind the Singleton pattern is very simple: There are some things that are unique. If you only ever have one instance of a class and a lot of code that needs access to that instance, it seems silly to pass the object from one method to another. In this kind of situation, the GoF suggest that you build a singleton - a class that can have only one instance and that provides global access to that one instance.

GoF recommended: Let the class of the singleton object manage the creation and access to its sole instance.

Class Variables and Methods

A class variable is a variable that is attached to a class instead of to an instance of the class. Creating a class variable is very straightforward: You simply add another at sign to the variable name: @@variable_name.

Class methods:

def self.class_method; end

A First Try At A Ruby Singleton

A non-singleton version:

class SimpleLogger
  attr_accessor :level

  ERROR = 1
  WARNING = 2
  INFO = 3

  def initialize
    @log = File.open("log.txt", "w")
    @level = WARNING
  end

  def error(msg)
    @log.puts(msg)
    @log.flush
  end

  def warning(msg)
    @log.puts(msg) if @level >= WARNING
    @log.flush
  end

  def info(msg)
    @log.puts(msg) if @level >= INFO
    @log.flush
  end
end

You might use this version of the logger by creating a new one and passing it around:

logger = SimpleLogger.new
logger.level = SimpleLogger::INFO

logger.info('Doing the first thing')
# Do the first thing...
logger.info('Now doing the second thing')
# Do the second thing...

Managing the Single Instance

The whole point of the singleton pattern is to avoid passing an object like the logger all over the place. Instead, you want to make the SimpleLogger class responsible for managing its single instance.

class SimpleLogger

  # Lots of code deleted...

  @@instance = SimpleLogger.new

  def self.instance
   @@instance
  end
end

We can now call the instance method of the SimpleLogger class any number of times and always get back the same logger object:

logger1 = SimpleLogger.instance # Returns the logger
logger2 = SimpleLogger.instance # Returns exactly the same logger

More practically, we can get at the singleton logger from anywhere in our code and use it to write out messages:

SimpleLogger.instance.info('Computer wins chess game.')
SimpleLogger.instance.warning('AE-35 hardware failure predicted.')
SimpleLogger.instance.error(
   'HAL-9000 malfunction, take emergency action!') 

Make sure there is only one

Remember, one requirement of the singleton is to ensure that the one and only singleton is the sole instance of the singleton class. We do so by making the new method on SimpleLogger private:

class SimpleLogger

  # Lots of code deleted...

  @@instance = SimpleLogger.new

  def self.instance
    @@instance
  end

  private_class_method :new
end

We made the new class method private, preventing any other class from creating new instance of our logger.

The singleton module

require 'singleton'

class SimpleLogger
  include Singleton

  # Lots of code deleted...

end

Alternatives to the classic singleton

  1. Global Variables as Singletons.
  2. Classes as Singletons
class ClassBasedLogger
  ERROR = 1
  WARNING = 2
  INFO = 3
  @@log = File.open('log.txt', 'w')
  @@level = WARNING

  def self.error(msg)
    @@log.puts(msg)
    @@log.flush
  end

  def self.warning(msg)
    @@log.puts(msg) if @@level >= WARNING
    @@log.flush
  end

  def self.info(msg)
    @@log.puts(msg) if @@level >= INFO
    @@log.flush
  end

  def self.level=(new_level)
    @@level = new_level
  end

  def self.level
    @@level
  end
end
  1. Modules as Singletons.

Abusing The Singleton Pattern

They are really just global variables, right? - Don't do that.

Just How Many of These Singletons Do You Have? - As you are considering applying the Singleton pattern, ask yourself this question: Am I sure that there is only one of these things? The Singleton pattern gives us a way to model a single instance of something, but this modeling also just happens to come with a nice coding feature that makes that single instance very easily accessible—just call SimpleLogger.instance. That easy access can have a hypnotic allure: "My code will be so much simpler if this thing is a singleton." Don’t listen to the siren song of that easy access. Instead, focus on the question of how many of these things exist and treat the easy access as a bonus.

viphat commented 6 years ago

Are singletons evil?

Singletons vs global variables

Many developers consider the Singleton pattern to be an anti-pattern. This is because, with its single object and global scope, a singleton shares key attributes with the much-maligned global variable. The intent of the Singleton pattern, however, is to model an object which occurs exactly once in a system. Unlike a global variable, it is not meant to be used as a method of globally connecting parts of a program and communicating between them, nor should it be used that way. Creating such communication across disparate pieces of software will create a tight coupling amongst those pieces. That is bad, and indeed, that is an anti-pattern. That is why global variables are almost universally feared. With design patterns, you as the software developer are responsible for fulfilling the intent and holding to the constraints of a given pattern. Singletons are not evil. Use them wisely. Use the correctly. Use them as they were designed to be used.