Reimplementando o operador de igualdade (==) no Ruby
15 Jul 2022
Numa postagem anterior falei sobre como o Ruby implementa o nil de forma correta. Nesta vou falar sobre o operador de igualdade (==) (não sei se essa é a tradução correta, caso tenha outra melhor, me avise) que o Ruby também implementa de forma muito correta. No entanto, nem sempre queremos que ele tenha o comportamento padrão, e aí chega a hora de reimplementar o operador nos nossos próprios termos.
Esse foi um conhecimento que adquiri realizando o teste da Husky, e resolvi compartilhar a solução em português pois me aprofundei bastante no assunto e atualmente a melhor postagem é uma do blog do Shopify, apenas em inglês. Eles lidam com dois casos: um para reimplementar o operador em entidades (models do Ruby on Rails, por exemplo) e outra para reimplementar em objetos de valor (código Ruby puro, por exemplo). Vou abordar nesta postagem apenas os objetos de valor.
O problema
Vou assumir que você já sabe a função básica do ==. Usamos a todo momento quando queremos, por exemplo, saber se dois Livros tem a mesma editora, usando arte_da_guerra.editora == harry_potter.editora.
Mas digamos que você queira comparar dois objetos de uma mesma classe, os dois com os mesmos argumentos. Você pode querer que eles sejam considerados iguais, não é?
carlos = Funcionario.new('Carlos', 'Marketing')
carlos_dois = Funcionario.new('Carlos', 'Marketing')
carlos == carlos # => retorna true
carlos == carlos_dois # => retorna false???
Isso acontece porque o Ruby vai dizer que dois objetos são iguais apenas nos casos em que eles são os mesmos objetos. Mas eu quero que ele considere que o carlos e o carlos_dois são iguais, até para eu poder evitar uma duplicação indesejada no futuro.
O Ruby não nos retorna true para essa comparação pois a implementação padrão vai buscar apenas se os dois objetos tem o mesmo id.
Como eles são objetos com id's diferentes o Ruby nos diz que eles não são iguais, mesmo que os parâmetros tenham os mesmos valores.
A solução
É aqui que entra o papel de redesenhar o operador de igualdade. O que nós queremos é que os valores sejam comparados, e se os valores forem iguais e da mesma classe, o == vai retornar true.
A diferença das implementações.
Primeiro, vamos recriar o método dentro da nossa classe:
class Funcionario
attr_reader :nome, :time
def initialize(nome, time)
@nome = nome
@time = time
end
def ==(outro)
end
end
Definimos o método == com um parâmetro chamado outro, que vai ser o objeto de comparação. O primeiro passo agora é conferir se o nosso primeiro objeto é da mesma classe do que o objeto outro de comparação.
def ==(outro)
self.class == outro.class
end
Ao fazer apenas isso, vamos ter os seguintes resultados:
carlos = Funcionario.new('Carlos', 'Marketing')
jose = Empregado.new('José', Marketing')
carlos_dois = Funcionario.new('Carlos', 'Marketing')
marina = Funcionario.new('Marina', 'Vendas')
carlos == jose
# => retorna false, pois são de classes diferentes
carlos == carlos_dois
# => retorna true, mas não pelos motivos corretos
carlos == marina
# => retorna true, pois são da mesma classe,
# mas não queremos pois tem valores diferentes
Então agora está faltando a gente comparar os valores, pois não queremos que carlos e marina sejam iguais, apenas que carlos e carlos_dois sejam iguais. Para isso, vamos adicionar um operador de conjunção:
Agora ele está comparando se os parâmetros nome e time também são iguais nos dois objetos, resultando no comportamento desejado:
carlos == jose
# => retorna false, pois são de classes diferentes
carlos == carlos_dois
# => retorna true, pois tem os mesmos valores
carlos == marina
# => retorna false, pois tem valores diferentes
Testando vários casos
Para provar que essa implementação funciona em vários casos, vamos testar algumas coisas:
carlos == Funcionario.new('Carlos', 'Marketing')
# retorna true pois tem os mesmos valores
carlos != carlos_dois
# retorna false, pois tem os mesmos valores
Para mais uma prova, vamos refazer apenas com um valor, considerando que não existe mais o parâmetro time na nossa classe Funcionario.
def ==(outro)
self.class == outro.class &&
@nome == outro.nome
end
Agora se fizermos mais um teste:
carlos = Funcionario.new('Carlos')
carlos == 'Carlos'
# retorna false, pois são de classes diferentes,
# o primeiro é da classe Funcionario, o segundo é
# da classe String padrão do Ruby.
Como o ActiveRecord reescreve o ==
Se você quiser ir além e reescrever esse código para entidades (models do Rails, por exemplo), você vai precisar adicionar um super (que irá retornar true se forem os mesmos objetos) e conferir se, de fato, o id não é nil. É assim que o ActiveRecord reescreve o operador de igualdade:
def ==(comparison_object)
super ||
comparison_object.instance_of?(self.class) &&
!id.nil? &&
comparison_object.id == id
end
alias :eql? :==
O instance_of? faz a mesma coisa que fizemos acima: self.class == comparison_object.class. Para se aprofundar, veja o post do Shopify.
É isto! Gostei de ter feito esse desafio, e espero que essa postagem seja útil para alguém que queira fazer isso no futuro. Caso encontre algum erro, por favor, me avise. Até mais!
Receba atualizações por e-mail
Sempre que tiver novidades por aqui vou enviar aos inscritos.
Entre na conversa
Criei uma postagem lá no LinkedIn para receber comentários: