Ruby é cheio de características interessantes. Uma delas, muito importante, é a flexibilidade.
Por exemplo, as operações aritméticas em ruby (+, -, *, /) não são definidas por operadores reservados da linguagem. São definidas como métodos. Veja; quando executamos o código a seguir:
2 + 2
O que o interpretador entende é análogo a
2.+(2)
Autor(a)
eric
Inscreva-se em nossa Newsletter
Fique por dentro de conteúdos, insights e oportunidades do universo tech. Receba novidades e lançamentos direto no seu e-mail.
A primeira versão do código é apenas uma maneira mais limpa de representar a segunda.
Ok, fica mais limpo. Mas onde está a flexibilidade?
Como + é um método como outro qualquer, é possível que ele seja definido ou redefinido por um código nosso. Neste caso, o método + está definido na classe Fixnum, que define o comportamento do números inteiros. Nesse caso específico do objeto '2'. Sim, o literal '2' é uma instância de Fixnum.
Para exemplificar esse tipo de flexibilidade, vamos só por diversão criar uma classe que represente um intervalo de tempo em minutos. Quero usá-la da seguinte forma:
O método horas retorna o resultado inteiro da divisão do total de minutos por 60. E o método minutos retorna o resto inteiro dessa divisão.
Legal! Mas é meio cansativo ficar digitando "#{intervalo.horas}:#{intervalo.minutos}" em todo lugar que quisermos exibir um intervalo. Seria mais interessante poder digitar apenas intervalo.to_s. Ou seja, invocar a representação String do nosso objeto intervalo e ter o mesmo efeito.
No entanto, se executarmos o intervalo.to_s, vamos perceber que o retorno é algo como "#", e não o desejado 1:30.
Isso acontece porque o interpretador simplesmente não foi ensinado a representar como String um objeto da classe Intervalo. Ele simplesmente utiliza a definição presente na classe Object, ou seja, ele utiliza a implementação herdada do método to_s definida para qualquer Object. Precisamos, portanto, redefinir o método to_s para ter um comportamento específico na classe Intervalo. Simples:
É mais interessante que os minutos sempre tenham duas casas. Para que isso aconteça, podemos usar sprintf que aceita uma máscara para definir a formatação. Logo, nosso método to_s fica melhor assim:
Perceba que nosso objeto intervalo é imutável. A operação de adição retorna um novo objeto em vez de alterar o objeto original. Essa é uma prática que visa reduzir a complexidade do código.
Dá para ir mais longe. O que aconteceria se tentássemos somar dois intervalos?
Temos uma mensagem de erro que a príncipio parece meio críptica: "Intervalo can't be coerced into Fixnum". No entanto, ela revela que o interpretador está tentando 'coerce' ou seja forçar nosso objeto Intervalo a ser tratado como um objeto do tipo Fixnum (numérico). Por que isso acontece? Vamos olhar para dentro do método Intervalo#+. Para simplificar o entendimento, vamos expandir o método pra realizar seu trabalho em duas linhas.
No último exemplo, perceba que dentro do método Intervalo#+ quando fazemos a soma
novo\_valor = @minutos + outros\_minutos
Temos @minutos como sendo um objeto da classe Fixnum, ou seja, um número inteiro. E outros_minutos como sendo um objeto da classe Intervalo. Logo nosso problema, para simplificar, deriva da seguinte situação: Já é possível fazer a soma Intervalo + Fixnum; porém não é possível fazer a soma Fixnum + Intervalo. Veja:
Ou seja, é chamado o método + definido na classe Fixnum. Esse método está definido no core da linguagem e não faz nenhuma idéia do que possa ser um Intervalo.
No entanto, os idealizadores do Ruby pensaram numa maneira bem interessante de flexibilizar o comportamento do método Fixnum#+. Como a classe Fixnum não sabe somar Intervalos, ela delega esta responsabilidade de volta para a classe Intervalo. Ou seja, quando um Fixnum está sendo somado a algo que o Ruby não reconhece como número, ele solicita ao objeto desconhecido que devolva dois objetos compativeis. Isso é realizado por um método chamado coerce. A responsabilidade desse método é devolver dois objetos que possam ser somados. Por exemplo.
Internamente, o objeto 30 vai chamar o método coerce do objeto intervalo para ter como retorno um par de objetos compatíveis com a operação de soma. Algo como
intervalo.coerce(30) # => ```30, 90
Perceba agora que a operação de soma prosseguirá dentro do método Fixnum#+ usando estes dois objetos, que são perfeitamente 'somáveis'.
30.+(90) # => 120
Assim sendo a operação toda se resolve e temos como resultado da soma Fixnum + Intervalo um número inteiro, ou seja, um Fixnum.
intervalo = Intervalo.new(90) soma = 30 + intervalo # ocorre uma chamada para intervalo.coerce(30) cujo retorno é ```30, 90
e em seguida 30.+(90) retornando 120 puts soma # => 120
Ótimo! A classe Fixnum está disposta a ser interoperável com qualquer outra classe. Desde que lhe seja dada uma maneira de entender como os objetos dessa nova classe querem ser somados a um Fixnum.
Bacana! Mas dá pra ficar melhor. A situação que temos agora é:
Meio esquisito. Seria mais interessante que indepententemente da ordem dos operadores o resultado fosse sempre um Intervalo. E é aí que está o pulo do gato.
Perceba que podemos controlar completamente como vai ser a operação de soma realizada pelo Fixnum. Basta que nosso método coerce retorne um par de objetos apropriado.
Neste momento nosso método Intervalo#coerce retorna 30, 90 , ou seja, Fixnum, Fixnum
. O que aconteceria se retornássemos ```Intervalo, Fixnum
? Ou seja
classIntervalodefcoerce(numero\_inteiro) ```Intervanlo.new(numero\_inteiro), @minutos
end end
Invertemos a soma!
30 + Intervalo.new(90)
Para algo como
Intervalo.new(30) + 90
Isso é lindo e genial! Pois o controle da operação de soma do Fixnum volta para o método + da classe Intervalo! E nesse método a operação de soma já está corretamente definida e retorna um objeto do tipo Intervalo :]
Para que isso seja possível, precisamos olhar para nossa implementação do método Intervalo#+
classIntervalo# ... def +(outros\_minutos) novo\_valor = @minutos + outros\_minutos Intervalo.new(novo\_valor) end end
Agora, novo_valor pode ser Fixnum (quando estamos somando Intervalo + Fixnum) ou Intervalo (quando estamos somando Fixnum + Intervalo e usando coerção). Neste segundo caso, quando novo_valor é do tipo Intervalo, vamos ter um problema ao fazer Intervalo.new(novo_valor), pois o método Intervalo#initialize
Guardaria a representação interna @minutos como sendo um Intervalo. E @minutos deve ser sempre um Fixnum. Como resolver isso?
Para garantir que @minutos seja sempre do tipo Fixnum, basta forçar que o parâmetro minutos seja convertido para Fixnum indepententemente de sua natureza. Garantimos isso chamando minutos.to_i
Portanto, quando um objeto do tipo Intervalo precisar ser representado como um inteiro, podemos fazer que ele devolva o número absoluto de minutos que o compõe. A convenção em Ruby para que um objeto seja convertido para número inteiro é que sua classe exponha um método chamado to_i. Logo
classIntervalo# ... def to\_i @minutos end end
Como estamos seguindo a conveção de definir um método chamado to_i, não precisamos nos preocupar quando o parâmetro minutos do construtor é do tipo Fixnum, pois nesta classe o método to_i já está definido e retorna o próprio objeto em que foi chamado. Bonito, não?