Menu
Coddy logo textTech

Singleton Pattern

Part of the Object Oriented Programming section of Coddy's Ruby journey. Lesson 42 of 57.

A design pattern is a proven solution to a common design problem. The singleton pattern makes sure a class has exactly one instance, reachable from anywhere. Ruby's standard library has it built in: require 'singleton' and include Singleton add a class method instance:

require 'singleton'

class Settings
  include Singleton
  attr_accessor :theme

  def initialize
    @theme = "light"
  end
end

Settings.instance.theme = "dark"
puts Settings.instance.theme
puts Settings.instance.equal?(Settings.instance)

Output:

dark
true

include Singleton also makes new private, so no second instance can be created. Every part of the program that asks for instance gets the same object:

require 'singleton'

class Counter
  include Singleton
  attr_reader :hits

  def initialize
    @hits = 0
  end

  def hit
    @hits += 1
  end
end

def page_a
  Counter.instance.hit
end

def page_b
  2.times { Counter.instance.hit }
end

page_a
page_b
puts Counter.instance.hits
begin
  Counter.new
rescue NoMethodError => e
  puts e.message
end

Output:

3
private method `new' called for class Counter

The same idea written by hand is a class method that creates the instance on first use and a private new. This shows what the module does for you:

class Config
  def self.instance
    @instance ||= new
  end
  private_class_method :new

  attr_accessor :level

  def initialize
    @level = 1
  end
end

Config.instance.level = 3
puts Config.instance.level

Output:

3

Use singletons sparingly. A singleton is global state: any code can change it, and tests cannot start from a clean copy. They fit things that really exist once per program, such as a logger or the configuration; passing an object as a parameter is often clearer:

# easier to test: the logger is passed in
def process(order, log)
  log.write("processing #{order.id}")
end
challenge icon

Challenge

Easy

Complete the singleton AppLogger with include Singleton. It keeps an array of entries and has log(message, level), which stores [INFO] message or [ERROR] message (the level in capitals), entries, and error_count. The helpers handle_info(message) and handle_error(message) in functions.rb both log through AppLogger.instance, so they reach the same object.

The supplied code reads lines info,message or error,message, sends them through the helpers, prints every entry and errors: 1, and checks that AppLogger.new is refused.

Your code goes in app_logger.rb and functions.rb. main.rb holds the supplied input/output code and cannot be edited.

Try it yourself

require_relative 'app_logger'
require_relative 'functions'

# Supplied input/output code: keep it as it is
input = []
while (line = gets)
  input << line.chomp
end
input.each do |line|
  level, message = line.split(",", 2)
  level == "error" ? handle_error(message) : handle_info(message)
end
AppLogger.instance.entries.each { |e| puts e }
puts "errors: #{AppLogger.instance.error_count}"
begin
  AppLogger.new
  puts "new is allowed"
rescue NoMethodError
  puts "new is private"
end
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming

Practice on your own: Online Ruby compiler