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.
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 é?
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.
carlos.object_id => retorna 60 carlos_dois.object_id => retorna 80
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.
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:
def ==(outro) self.class == outro.class && @nome == outro.nome && @time == outro.time end
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!