DEV Community

João
João

Posted on

O mistério do objeto de Ruby escondido Singleton Class

Oi, espero que esteja pronto, porque agora iremos tentar entender o que é esse objeto "escondido" por todos os lugares quando programamos em Ruby.

Se você é um programador curioso, já sabe que Ruby é uma linguagem que utiliza o paradigma de Orientação à Objetos, mais que isso, em Ruby tudo é objeto, tudo mesmo. Quando definimos uma classe House, por exemplo, essa classe também é um objeto, uma instância de Class, podemos perceber isso com o trecho de código abaixo, onde chamamos o método .class que retorna a classe de um objeto.

class House; end

# House é uma instância da classe `Class`
House.class  # => Class
Enter fullscreen mode Exit fullscreen mode

Isso pode ser diferente das coisas que você já viu antes e funciona de forma diferente de como eu acreditava, então começei a me perguntar como essa característica da linguagem me impactava como programador? Eu não tinha uma resposta muito boa e procurei entender mais sobre esse comportamento, até que encontrei a chamada Singleton Class (também encontrei referências que chamam Eigenclass, Classe anônima e Object-Specific Class), a responsável por termos em Ruby os chamados métodos de classe, como o desse trecho abaixo.

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

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

Acredito que não há nada no código acima que seja diferente do que estamos acostumados escrever como programador Ruby. Singleton Classes nos permitem adicionar métodos em objetos pré-definidos que só afetarão o objeto que define este método, como no trecho abaixo.

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

Isso também explica como criamos o método de classe self.open do trecho anterior, pois como a classe House é um objeto da classe Class, então também utilizamos essa sintáxe (self.open) para adicionar um novo método nesse objeto pré-definido.

Entendi então que as sintaxes def self.open e def my_home.lawn_situation são responsáveis por acessar a Singleton Class desses objetos adicionandos estes métodos nelas e não diretamente nas suas classes, permitindo que estes novos métodos afetem apenas os seus respectivos objetos.

Acredito que nesse momento conseguimos compreender o que é esse objeto chamado Singleton Class e como utilizamos, mas ainda não discutimos sobre como ele nos impacta na vida real, pois faremos isso agora.
Em conversa com alguns colegas, foi citado que um possível uso para essa característica da linguagem seria sobreescrever métodos de objetos para auxiliar alguns testes. No exemplo a seguir, utilizamos a Singleton Class para sobreescrever um método com o objetivo de auxiliar o teste de um trecho código.

# Definindo a classe House
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

# Testando a classe House
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

Particularmente, saber dessa característica de Ruby, me fez entender mais claramente um bug que criei ao desenvolver um comportamento, vou tentar apenas ilustrar o problema ocorrido em um contexto mais simples no trecho abaixo.

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} já possui o livro '#{@book}'"
    end

    private

    def self.friend_has_this_book?
        puts "Verificando se #{@friend.name} já possui o livro '#{@book}'"
        @friend_has_this_book ||= @friend.books.include?(@book)
    end

    def self.send_now(book)
        puts "Enviando o livro '#{book}' para #{@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('João')
guilherme = Friend.new('Guilherme')

Book.gift_a_friend(joao, 'Lord of the Rings')
# Verificando se João já possui o livro 'Lord of the Rings'
# Enviando o livro 'Lord of the Rings' para João!

Book.gift_a_friend(joao, 'Lord of the Rings')
# Verificando se João já possui o livro 'Lord of the Rings'
# João já possui o livro 'Lord of the Rings'

Book.gift_a_friend(guilherme, 'Lord of the Rings')
# Verificando se Guilherme já possui o livro 'Lord of the Rings'
# Guilherme já possui o livro 'Lord of the Rings'

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

# WTF? Guilherme não possui o livro 'Lord of the Rings'
Enter fullscreen mode Exit fullscreen mode

O problema é que, na segunda vez em que o método .friend_has_this_book? for chamado, ele não vai executar @friend.books.include?(@book) pois o valor @friend_has_this_book já foi preenchido, pois a Singleton Class é criada apenas uma única vez (por isso o nome Singleton), as chamadas seguintes apenas acessarão a Singleton Class já existente mantendo seu estado.

Eu gostaria de ter entendido bem antes esse conceito da linguagem, mas é um ótimo ponto de partida pra quem quer conhecer mais profundamente a linguagem Ruby e suas características, com certeza esses entendimentos irão evitar a os erros e ajudá-lo na análise dos problemas que você enfrentará como programador.
Espero que tenha gostado, até a próxima.

Algumas fontes que eu utilizei para entender um pouco mais sobre esse assunto:

Top comments (0)