Practical Applications of the Singleton Class in Ruby

AuthorMáximo Mussini
·6 min read

In the previous post, we discussed how the singleton class powers class methods in Ruby, and how every object instance has its own singleton class.

In this post, we will cover a few practical usages of the singleton class as a way to modify the behavior of a particular object.

Adding Methods and Mixins

Since the singleton class of an object is specific to it, we can add methods to it, remove methods, or include modules, all without affecting other instances.

When calling a method on an object, Ruby will first look into its singleton class to find it, before traversing the rest of the method chain.

Defining Singleton Methods

Let's cover a few of the syntaxes we can use to define methods for a specific object.

person = Object.new

person.define_singleton_method(:name) {
  'Alice'
}

person.singleton_class.define_method(:to_s) {
  "#{ name } in #{ location }"
}

def person.location
  'Wonderland'
end

class << person
  def inspect
    "#{ to_s }: #{ super }"
  end
end

person.inspect # => "Alice in Wonderland: #<Object:0x00007fe7b5071238>"

The last two syntaxes should be familiar, we usually use them inside a class definition with self as the receiver (inside the class definition self is the class object).

These four different ways of defining a method are equivalent, each one is defining a singleton method.

A singleton method is a method defined in the singleton class of an object.

As we saw in the previous post, class methods are simply singleton methods of a class object, which explains why the same syntaxes can be used with a different receiver: they do the same thing.

Adding Mixins to the Singleton Class

We are not limited to adding or overriding methods, we can also work with modules.

module Greeting
  def introduce
    "Hey, I'm #{ name }"
  end
end

module NiceGreeting
  def introduce
    "#{ super }, nice to meet you!"
  end
end

person.extend(Greeting)
person.singleton_class.include(NiceGreeting)

person.introduce
# => "Hey, I'm Alice, nice to meet you!"

person.singleton_class.ancestors
# => [#<Class:#<Object:...>>, NiceGreeting, Greeting, ...

The example above illustrates how module inheritance works when dealing with the singleton class. Using extend is like including a module in the singleton class.

Calling extend on an object will make the module methods available on that object. In a class definition the object is implicit: the class object.

Practical Applications

Let's now dive into a few scenarios where all of this flexibility becomes useful.

Test Doubles and Method Stubbing

A test double is any object that stands in for a real object during a test. Some libraries allow to easily create doubles, and stub some of their methods:

book = instance_double('Book', pages: 236)
book.pages # => 236

A method stub is an instruction to an object to return a known value in response to a message:

allow(book).to receive(:title) { 'Free Play' }
book.title # => "Free Play"

Most libraries implement both of these features by leveraging the singleton class.

Let's see how we might be able to implement a very simplistic version of double, which returns an object that can respond to the specified methods:

def double(name, **method_stubs)
  Object.new.tap do |object|
    object.instance_variable_set('@name', name)
    method_stubs.each do |name, value|
      object.define_singleton_method(name) { value }
    end
  end
end

book = double('Book', pages: 236, title: 'Free Play')
book.pages # => 236
book.title # => "Free Play"

By using define_singleton_method we can create a test double that conforms to the provided options, without having to use temporary classes or structs, nor affecting other object instances.

RSpec Example Group Methods

When writing tests with RSpec, it's a good practice to keep helpers and state as local as possible. A typical way to do that is to include helpers only for certain types of tests.

RSpec.configure do |config|
  config.include(EmailSpec::Helpers, type: :controller)
end

Behind the scenes, RSpec will leverage the singleton class of a specific example group to include the module, without affecting other test scenarios.

RSpec.configure do |config|
  config.before(:each, type: :controller) do |example|
    example.example_group_instance.singleton_class.include(EmailSpec::Helpers)
  end
end

We can use this to our advantage as a way to define scenario-specific methods as well:

RSpec.configure do |config|
  config.before(:each, :as) do |example|
    example.example_group_instance.define_singleton_method(:current_user) {
      instance_exec(&example.metadata[:as])
    }
  end
end

RSpec.feature 'Visiting a page' do
  before { sign_in_as current_user }

  it 'can visit the page as a user', as: -> { User.first } do
    ...
  end

  it 'can visit the page as an admin', as: -> { Admin.first } do
    ...
  end
end

Check this example in the Capybara Test Helpers library, which uses it to inject test helpers using a :test_helpers option.

RSpec.feature 'Cities' do
  scenario 'adding a city', test_helpers: [:cities] do
    visit_page(:cities)
    cities.add(name: 'Minneapolis')
    cities.should.have_city('Minneapolis')
  end
end

Custom Cache Keys

When using Rails' cache, fetch_multi supports passing a list of keys, which will be yielded to the block in order to calculate the value to cache.

keys = items.map { |item| cache_key_for(item) }
Rails.cache.fetch_multi(*keys) { |key| value_for(key) }

What if we need the item instead of the key in order to calculate the value to cache?

items_by_cache_key = items.index_by { |item| cache_key_for(item) }
cache_keys = items_by_cache_key.keys
Rails.cache.fetch_multi(*cache_keys) { |key| value_for(items_by_cache_key[key]) }

Quite awkward. However, fetch_multi also supports passing a list of objects, in which case it will call cache_key on the objects to obtain the cache keys.

Rails.cache.fetch_multi(*items) { |item| value_for(item) }

But what if the items don't respond to cache_key?

We can leverage define_singleton_method to define it differently for each item:

items.each do |item|
  item.define_singleton_method(:cache_key) { cache_key_for(item) }
end
Rails.cache.fetch_multi(*items) { |item| value_for(item) }

Check this example in the oj_serializers library, which defines a cache_key singleton method for each object, so that they can be passed to fetch_multi.

Ad Hoc Validations in Rails

Let's imagine that we have a file upload API, and we are running an integrity check on save.

module FileIntegrityValidation
  extend ActiveSupport::Concern

  included do
    validate { errors.add(:file, 'is corrupt') if corrupt? }
  end

  def corrupt?
    ...
  end
end

What if we need to run the validation conditionally based on a setting that is not accesible from the model?

We can leverage the singleton class to define the validation conditionally:

class Api::FilesController < Api::BaseController
  resource(:file_upload)

  def create
    if check_file_integrity?
      file_upload.singleton_class.include(FileIntegrityValidation)
    end

    if file_upload.save
      ...

The resource syntax is coming from resourcerer.

In this case we can't use extend because ActiveSupport::Concern internally uses the included hook, which is only triggered when using include.

When using this pattern, it's better to encapsulate it so that it's easier to understand:

  def create
    if check_file_integrity?
      FileIntegrityValidation.apply_on(file_upload)
    end
module FileIntegrityValidation
  # Public: Define this validation only for the provided object.
  def self.apply_on(object)
    object.singleton_class.include(self)
  end
end

Summary

We can use an object's singleton class to define methods or include modules without affecting other instances, which enables very powerful techniques such as method stubbing and dynamically modifying behavior.

In practice, the use cases where this is necessary don't come around very often. It's usually possible to achieve what we need using simpler or more explicit strategies, that are easier to reason about.

As with most advanced techniques, you will know when you need it 😃