DEV Community

João
João

Posted on

The mistery of the hidden Ruby object called Singleton Class

Hi, I expect are you ready, now we will try to understand what is this "hidden" object in all places when we code in ruby.

If you are a curious programmer, you know that Ruby is a Object Oriented programming language, and beyond, everything is an object in ruby, everything. When we define a House class, for exemple, this class is also an object, a Class instance. We can see this in the code snippet below, where we call the .class method returning the class name of this object.

class House; end

# House is an intance of `Class`
House.class  # => Class
Enter fullscreen mode Exit fullscreen mode

This can be different you have seen before and works different from how I thought, so I started to ask how this language feature impacted me as a programmer? I didn't have a good answer and I searched more about this behaviour, until I found the Singleton Class (I also found references calling Eigenclass, Anonymous Class and Object-Specific Class), the responsible for the ruby class methods, like the code snippet below.

class House
    def self.open
        puts "...Opened"
    end
end

House.open  # => ...Opened
Enter fullscreen mode Exit fullscreen mode

I believe there is nothing in this code different of we use to write ruby code everyday. Singleton Classes allow us to add methods to pre-defined objects that only affect the object that defines this method, like the code snippet below.

class House
    def lawn_situation
        puts "...this is perfect"
    end
end

my_home = House.new
neighbors_house = House.new

def my_home.lawn_situation
    puts "...could be better"
end

neighbors_house.lawn_situation  # => ...this is perfect
my_home.lawn_situation  # => ...could be better
Enter fullscreen mode Exit fullscreen mode

This also explain how we create the self.open method in the previous code, since the House class is an object of the Class class, so we also use this syntax (self.open) to add a new method to this pre-defined object.

Now, I understand that the syntaxes def self.open and def my_home.lawn_situation are responsible for accessing the Singleton Class of these objects to add the methods and not directly to their classes, allowing these new methods to only affect their respective objects.

I think now we can understand what is this object called Singleton Class and how we use it, but we haven't discussed how it impacts our real life, so we will do it now.
In a conversation with some friends, it was mentioned a use case to this feature, we can to override object methods to help us to create some specs. Below, we used the Singleton Class to override a method to help us to test a code behaviour.

# Defining House class
class House
    def self.close
        self.close_back_door
        self.close_windows
        self.close_principal_door
        nil
    end

    def self.close_back_door = puts "...Closed Back Door"
    def self.close_windows = puts "...Closed Windows"
    def self.close_principal_door = puts "...Closed Principal Door"
end

# Testing .close method
context "Error to close the house" do
    def House.close_principal_door
        raise EspecificException, "Error to close the principal door"
    end

    expect(House.close).to raise_error(EspecificException)
end
Enter fullscreen mode Exit fullscreen mode

Particularly, knowing this ruby feature, made me understand clearly about a bug I created when I was developing a new system feature, I will try to illustrate the problem in a simpler context.

class Book
    def self.gift_a_friend(friend, book)
        @friend = friend
        @book = book

        return send_now(@book) unless friend_has_this_book?

        puts "#{@friend.name} already has the book '#{@book}'"
    end

    private

    def self.friend_has_this_book?
        puts "Verifying if #{@friend.name} already has the book '#{@book}'"
        @friend_has_this_book ||= @friend.books.include?(@book)
    end

    def self.send_now(book)
        puts "Sending the book '#{book}' to #{@friend.name}!"
        @friend.add_book(book)
    end
end

class Friend
    attr_reader :name, :books

    def initialize(name, books = [])
        @name = name
        @books = books
    end

    def add_book(book)
        @books << book
    end
end

joao = Friend.new('J. Moura')
guilherme = Friend.new('Guilherme')

Book.gift_a_friend(joao, 'Lord of the Rings')
# Verifying if J. Moura already has the book 'Lord of the Rings'
# Sending the book 'Lord of the Rings' to J. Moura!

Book.gift_a_friend(joao, 'Lord of the Rings')
# Verifying if J. Moura already has the book 'Lord of the Rings'
# J. Moura already has the book 'Lord of the Rings'

Book.gift_a_friend(guilherme, 'Lord of the Rings')
# Verifying if Guilherme already has the book 'Lord of the Rings'
# Guilherme already has the book 'Lord of the Rings'

guilherme
# => #<Friend:0x000079bd4d8e6d20 @books=[], @name="Guilherme">

# WTF? Guilherme has not the book 'Lord of the Rings'
Enter fullscreen mode Exit fullscreen mode

The problem is in the second time when the .friend_has_this_book? method is called, it will not execute @friend.books.include?(@book) because the @friend_has_this_book instance variable value has already been set. The Singleton Class is created only once (this explains the name Singleton), the following calls will only access the existing Singleton Class keeping its state.

I would like to understood it before, but it is a good starting for who want to know deeper about the ruby language and its features, these knowledge will avoid the errors and help us to analyze the problems you will face as a programmer.
I hope you enjoyed, see you.

Some sources I used to understand about this subject:

Top comments (0)