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 😃