O que tem de diferente no Ruby?
Todos os códigos Ruby mostrados neste artigo podem ser testados no ambiente IRB (Interpreted Ruby). Com o Ruby já instalado em sua máquina, digite o comando ‘irb’ e você estará dentro do ambiente dinâmico do interpretador Ruby. Qualquer código Ruby será executado na hora, o que deve facilitar seus testes. Na realidade eu recomendo que à medida que leiam o artigo, digitem os códigos em Ruby no IRB e vejam vocês mesmos os resultados. Será bem mais educativo desta forma.
Para começar, vejam este código:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Pessoa def initialize(nome) @nome = nome end def nome @nome end def nome=(valor) @nome = valor end end fabio = Pessoa.new("Fabio") puts fabio.nome |
Antes de mais nada, algumas explicações: o que outras linguagens chamam de funções ou métodos, em Ruby são definidas com ‘def … end’. Todo bloco de código termina com ‘end’.
Para colocar um método todo na mesma linha você poderia separar cada linha com “;” (ponto-e-vírgula):
rubydef nome; @nome; end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
Para que um método retorne um valor, não é necessário usar a palavra 'return' como estamos acostumados. O resultado da última expressão de um método sempre é devolvida no retorno. Por exemplo, o método 'nome' acima tem apenas '@nome', que é o equivalente a fazer 'return @nome'. Use 'return' apenas se quiser sair do fluxo do método antes de chegar à linha final. Finalmente, variáveis de instância (algo como 'this.nome') são denotados com o prefixo '@'. Para instanciar uma classe, basta chamar o método 'new' dela. Os parâmetros passados a esse método são repassados como argumentos ao método padrão 'initialize'. E 'puts' é apenas algo parecido com 'System.out.println()'. Parênteses são opcionais, só os use para eliminar ambigüidades na hora de chamar um método com parâmetros. À primeira vista, o código acima não é diferente de algo semelhante em Java: --- java class Pessoa { String nome; public Pessoa(String nome) { this.nome = nome; } public String getNome() { return this.nome; } public void setNome(String nome) { this.nome = nome; } } Pessoa fabio = new Pessoa("Fabio"); System.out.println(fabio.getNome()); |
Nem mesmo em número de linhas de código temos algum ganho. Olhando apenas dessa forma, para um iniciante, ficaria a pergunta: Onde está a diferença? Apenas por não ter chaves?
Bom, vejamos um outro exemplo – da mesma classe Ruby:
1 2 3 4 5 6 7 8 |
require 'ostruct' class Pessoa < OpenStruct; end fabio = Pessoa.new :nome => "Fabio" puts fabio.nome fabio.email = 'akitaonrails@mac.com' puts fabio.email |
Bom, agora estamos começando a conversar! No caso acima eu confesso, trapaceei um pouco. Fiz a classe ‘Pessoa’ herdar a partir de uma classe padrão do Ruby, a ‘OpenStruct’. Ela permite que os objetos instanciados a partir dessa classe tenham qualquer atributo que for necessário em tempo de execução! Vou voltar neste ponto mais tarde, por enquanto apenas ignore a magia negra e continue.
Ruby suporta herança simples de classes, assim como em Java ou C#. O caracter ‘<’ é o equivalente a ‘extends’ em Java.
Vamos a mais alguns exemplos simples:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
>> 1 + 2 => 3 >> numero = 5 => 5 >> numero * 3 => 15 >> contador = 1 => 1 >> while contador < numero >> puts contador >> contador += 1 >> end 1 2 3 4 >> def hello >> "Hello World" >> end >> puts hello Hello World |
Obs: quando você está no IRB, “>>” é o “prompt de comando” e “=>” é o resultado do comando que você acabou de executar. Não confunda com o código Ruby propriamente dito.
Na primeira parte algumas operações simples. Em seguida um loop tradicional, com contador. Depois um pequeno método chamado “hello” e sendo executado. Acredito que, independente da linguagem de onde você veio, o que mostrei até agora é bastante familiar. Novamente, fica a pergunta: “onde está a diferença?”
Quase tudo é Objeto
Ruby foi muito influenciado por Smalltalk, a verdadeira linguagem que inspirou quase todas as outras mais modernas de orientação-a-objetos. Não quero aqui começar nenhuma discussão sobre o que é ser OOP ou não pois argumentos desse tipo são irrelevantes aqui.
O que muitos costumam reclamar em linguagens tradicionais é o seguinte:
1 2 3 4 5 |
class Teste { public static void main(String[] args) { System.out.println("Hello World"); } } |
Muita coisa para fazer um simples “Hello World”. Agora vejamos o Hello World em Ruby:
1 |
puts "Hello World"
|
“Oras, mas Ruby não é OOP?? Cadê a classe!!?” Vamos entender o exemplo. Considerando que ainda estamos dentro do ambiente IRB, faça o seguinte:
1 2 3 4 5 6 7 8 |
>> puts "Hello World" Hello World => nil >> self => main >> self.class => Object |
Em Ruby, “self” é mais ou menos parecido com a função do “this” em Java ou outras linguagens. Mas como podem ver, nós já estamos dentro de um objeto, de uma instância de Object. Nunca estamos num ambiente estático ou não-objeto. Criar um método no IRB significa adicionar esse método ao objeto ‘main’ que, por sua vez é uma instância da classe Object.
Mas não é apenas isso, veja o seguinte:
1 2 3 4 5 6 7 8 9 10 11 |
>> 1.class => Fixnum >> true.class => TrueClass >> false.class => FalseClass >> nil.class => NilClass |
Um número, um booleano (verdadeiro, falso) e até nulo (o “nil”) são objetos. Todos eles respondem a métodos, em especial o método “class” que indica de que classe esse objeto é instância. Note que até o nulo é uma instância da classe chamada “NilClass”.
Classes Abertas
Uma coisa que não tem o que se fazer em muitas linguagens estáticas como Java é mudar de idéia. Toda a idéia de se ter interfaces e classes fechadas força o programador a decidir da primeira vez e ter que sobreviver às conseqüências de sua decisão, o que normalmente força ter que tomar a decisão certa logo no começo e incentiva o temido Big Design Up Front.
Quando precisamos de mais funcionalidades nesses casos, precisamos improvisar. Por exemplo:
1 2 3 4 5 6 7 8 |
class StringUtils { public static boolean isEmpty(String str) { return str == null || str.length() == 0; } } String nome = "Fabio Akita"; StringUtils.isEmpty(nome); |
O método “isEmpty()”, disponível na classe StringUtils do pacote Commons do Jakarta, avalia se o string que ele recebeu como argumento é nulo ou vazio. Na classe original “java.lang.String” não existia o método “isEmpty()”. Isso não é um erro, apenas na época em que essa classe foi criada, não parecia óbvio a ninguém que um método assim poderia ser útil. Mas agora, a classe String é fechada e não pode ser reimplementada. Também não adianta criar uma classe MyString que herda de String porque todas instâncias de String não terão as novas funcionalidades do MyString.
Portanto a solução-gambiarra significa criar uma classe separada “StringUtils”, com vários métodos estáticos, como o “isEmpty()”, o que efetivamente significa programação procedural e não-orientada a objetos. StringUtils é apenas uma estrutura com um punhado de métodos estáticos.
Em Ruby, podemos o mesmo problema resolver desta forma:
1 2 3 4 5 6 7 8 |
class String def empty? self.nil? || self.size == 0 end end nome = "Fabio Akita" nome.empty? |
Nesse caso específico nem precisaríamos fazer isso porque o método “empty?” já existe na String de Ruby, mas está aí apenas para ilustrar o problema.
Temos algumas coisas interessantes aqui. Primeiro, o nome do método pode parecer estranho por causa da interrogação no final. Mas não se assustem, em Ruby isso é um caracter válido num nome de método e não tem nenhum efeito colateral, é apenas um caracter a mais que expressa a intenção do método, no caso, de fazer uma pergunta ao objeto.
Mas o mais importante: hoje estamos decidindo que a classe String deveria ter mais métodos do que ele já tem. Felizmente, graças às características da linguagem, não estamos presos a uma classe fechada: podemos reabrí-la, reimplementá-la como quisermos e, automaticamente, toda instância de String (novas e inclusive as que já existiam), passam a responder à nova implementação.
Vejamos outro exemplo de orientação a objetos mesclado com “eye candy”.
1 2 |
>> 1 + 2 => 3 |
Vamos lembrar do básico: números, em Ruby, são objetos, mais especificamente instâncias da classe Fixnum. Agora vejamos outra maneira de escrever a mesma coisa em Ruby, sem “eye candy”:
1 2 |
>> 1.+(2) => 3 |
Espero que isso esteja claro: quando somamos dois números, na realidade estamos chamando o método especial “+” do objeto “1” e passando como argumento o objeto “2”. E isso vale para todos os operadores matemáticos que conhecemos como “-”, “/”. Mas o que acontece quando tentamos somar dois objetos incompatíveis?
1 2 3 4 5 6 7 8 9 |
>> 1 + "2" TypeError: String can't be coerced into Fixnum from (irb):35:in `+' from (irb):35 >> 1.+("s") TypeError: String can't be coerced into Fixnum from (irb):36:in `+' from (irb):36 |
Escrevi as duas maneiras novamente: passar um String como parâmetro ao método “+” de um Fixnum devolve uma exceção “TypeError” indicando que a operação é inválida. Mas digamos que, apenas por motivos didáticos, eu realmente queira que o Ruby reaja como Javascript ou Perl e que “1” seja convertido em String e depois concatenado ao parâmetro “2”, resultando em “12”.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
>> class Fixnum >> alias :soma_velha :+ ?> def +(valor) >> return self.to_s + valor if valor.is_a? String >> soma_velha(valor) >> end >> end => nil >> 1 + 2 => 3 >> 1 + "2" => "12" |
Várias coisas acontecendo aqui. Vejamos os principais pontos: primeiro, estamos reabrindo a classe Fixnum, ou seja, todas as instâncias dessa classe serão afetadas automaticamente pelo que faremos a seguir.
Depois, mais uma novidade: o método ‘alias’. Esse é um método de classe que cria um novo apontamento a um método que já existe. Pense em algo assim: você tem ‘a = foo’ e depois ‘b = a’. Agora, tanto ‘a’ quanto ‘b’ apontam para o mesmo objeto ‘foo’.
Outra coisa: quando uma classe é criada, reaberta ou ‘executada’ os métodos chamados dentro dela são executados. Ou seja, o comando ‘alias’, por exemplo, serve para reapontar um método com outro nome. No exemplo, já existia o método chamado “+” e com “alias” criamos um segundo método chamado “soma_velha” que aponta para a mesma implementação do método original. Ou seja, neste ponto as três chamadas a seguir se equivalem:
1 2 3 4 5 6 7 8 |
>> 5 + 10 => 15 >> 5.+(10) => 15 >> 5.soma_velha(10) => 15 |
Mas fizemos mais: depois de criar o novo apontamento “soma_velha” reimplementamos o método antigo “+”. A idéia é: se o parâmetro for um String, quero transformar o objeto Fixnum num String e depois concatenamos os dois. Veja como usamos o ‘return’ para retornar imediatamente caso este seja o caso. Senão, se o argumento passado não for um String, passamos para o método ‘soma_velha’ fazer a soma do jeito antigo.
Note também que usamos o ‘if’ de uma forma um pouco diferente: no fim da expressão:
1 |
return self.to_s + valor if valor.is_a? String |
‘is_a?’ é um método que está sem os parênteses (lembram-se? são opcionais) e serve para verificar o tipo do objeto ‘valor’. Literalmente podemos ler assim: “retorne self.to_s concatenado com ‘valor’ se for do tipo String.” Se você entende inglês verá que a expressão é praticamente uma frase.
E é isso que acontece agora quando tentamos somar um número com um string: ele retorna a concatenação de dois Strings. Note também que esse comportamento passa a valer para todos os objetos numéricos (instâncias de Fixnum).
Esse conceito de reabrir uma classe e implementar uma nova funcionalidade ficou conhecido com o nome de monkey patching. É um recurso muito poderoso e, como tudo que é poderoso ele pode ser tanto uma grande vantagem como um grande risco se usado da maneira errada. Espera-se, claro, que o programador não abuse disso.
O framework Ruby on Rails faz muito uso desse recurso. Um dos pacotes que compõe o Rails chama-se Active Support e uma de suas utilidades é justamente reabrir diversas classes padrão do Ruby para incorporar mais funcionalidades. Por exemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# carrega o pacote activesupport >> require 'rubygems' >> require 'activesupport' => true # horário atual >> Time.now => Wed Oct 29 23:36:24 -0200 2008 # fazendo cálculos com datas >> Time.now - 23.days => Mon Oct 06 23:36:28 -0300 2008 >> 2.weeks.ago => Wed Oct 15 23:36:32 -0200 2008 # fazendo cálculos com unidades de medida >> (1.gigabyte - 512.megabytes) / 1.kilobyte => 524288 # transformando objetos em XML >> { :html => { :body => { :p => "teste" } } }.to_xml => "<?xml version="1.0" encoding="UTF-8"?>\n <hash>\n <html>\n <body>\n <p>teste</p>\n </body>\n </html>\n </hash>\n" |
Como podem ver, podemos incrementar muito as funcionalidades de tudo que já existe. O Ruby on Rails começa exatamente assim: primeiro incorporando muitas coisas novas ao próprio Ruby e depois construindo sobre ela. Muitos são casos onde simplesmente depender de criar novas sub-classes não adiantaria muita coisa. Outro exemplo: em Java, se quisermos comparar o conteúdo de dois Strings, não devemos fazer isso:
1 2 3 4 5 6 7 8 |
String a = "foo"; String b = "bla"; if (a == b) { System.out.println("encontrado!"); } if (a > b) { System.out.println("a maior do que b"); } |
O correto seria assim:
1 2 3 4 5 6 |
if (a.equals(b)) { System.out.println("encontrado!"); } if (a.compareTo(b) > 0) { System.out.println("a maior do que b"); } |
Já, em Ruby, fazemos assim:
1 2 3 4 |
a = "foo" b = "bla" puts "encontrado!" if a == b puts "a maior do que b" if a > b |
Exatamente como imaginaríamos que deveria ser. Isso porque “==” e “>” são ‘operadores’ mas são também nomes de métodos, como explicamos acima. Então fica fácil implementar o comportamento que precisamos da maneira mais clara e expressiva possível.
Módulos e Organização
No exemplo do Fixnum, reabrimos diretamente a classe para colocar novas funcionalidades. Mas podemos fazer diferente:
1 2 3 4 5 6 7 |
Fixnum.class_eval do alias :soma_velha :+ def +(valor) return self.to_s + valor if valor.is_a? String soma_velha(valor) end end |
Como a classe “Fixnum” é por si mesmo um objeto, podemos chamar métodos nela. Por exemplo, “new” é um método dessa instância de Class. O que fizemos acima é a mesma coisa que fizemos antes, mas esse código podemos colocar dentro um método, para ser executado somente quando quisermos. Ou seja, podemos alterar o comportamento de uma classe programaticamente. Mas podemos ser ainda mais seletivos:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
>> a = "teste" => "teste" >> a.instance_eval do ?> def hello >> "hello from teste" >> end >> end => nil >> a.hello => "hello from teste" >> "foo".hello NoMethodError: undefined method `hello' for "foo":String from (irb):17 from :0 |
Veja agora: criamos um String na variável “a”. Então modificamos essa instância acrescentando um método chamado “hello”, mas somente a essa instância. Quando chamamos “a.hello” ele responde como esperamos, mas quando pegamos uma nova instância de String e tentamos chamar o mesmo método, vemos que não existe. Ou seja, podemos modificar o comportamento de todos os objetos de uma classe, ou somente de um único objeto individual.
Agora vejamos um outro meio de injetar código em classes de maneiras mais organizadas:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
module MeusPatches def say_hello "Hello World!" end def say_time Time.now end end class Fixnum include MeusPatches end class String extend MeusPatches end |
Módulos são como Classes que não podem ser instanciadas. No exemplo acima, organizamos dois métodos dentro de um módulo chamado “MeusPatches”. Em seguida reabrimos as classes “Fixnum” e “String”. No primeiro incluímos o módulo e no segundo extendemos o módulo. Para entender a diferença vamos usar isso:
1 2 3 4 5 6 7 8 9 |
>> 13.say_hello => "Hello World!" >> "".say_hello NoMethodError: undefined method `say_hello' for "":String from (irb):19 >> String.say_hello => "Hello World!" |
O objeto “13” (que é instância de Fixnum) responde ao método do módulo. A string vazia "" não responde. Eis a diferença entre “include” e “extend”: no segundo quem responde ao método do módulo é a própria classe. Na prática, pense que “include” serve para acrescentar métodos de instância e “extend” para acrescentar “métodos de classe” – esse não é o termo correto mas para a maioria dos casos serve.
No Ruby on Rails esse recurso é muito usado, principalmente para organizar códigos de classes muito longas. Por exemplo, o ActiveRecord tem centenas de funcionalidades. Colocar tudo numa única classe seria muito difícil de manter depois, por isso ele se organiza desta forma:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# ActiveRecord::Base.class_eval do extend ActiveRecord::QueryCache include ActiveRecord::Validations include ActiveRecord::Locking::Optimistic include ActiveRecord::Locking::Pessimistic include ActiveRecord::AttributeMethods include ActiveRecord::Dirty include ActiveRecord::Callbacks include ActiveRecord::Observing include ActiveRecord::Timestamp include ActiveRecord::Associations include ActiveRecord::NamedScope include ActiveRecord::AssociationPreload include ActiveRecord::Aggregations include ActiveRecord::Transactions include ActiveRecord::Reflection include ActiveRecord::Calculations include ActiveRecord::Serialization end |
Cada um dos includes acima tem um arquivo separado. Por exemplo ActiveRecord::QueryCache fica no arquivo “active_record/query_cache.rb”. É uma excelente maneira de organizar seus códigos. Mas existem alguns truques importantes de se conhecer. Uma delas é entender que módulos tem “eventos”. Ou seja, podemos instruir o módulo para executar alguma coisa toda vez que for incluso em alguma classe.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
module MeusPatches def self.included(base) base.send(:extend, ClassMethods) puts "Module MeusPaches incluso na classe #{base.name}" end def metodo_de_instancia "sou um metodo de instancia" end module ClassMethods def self.extended(base) puts "Module MeusPatches::ClassMethods extendido na classe #{base.name}" end def metodo_de_classe "sou um metodo de classe" end end end class Pessoa end ?> Pessoa.send(:include, MeusPatches) Module MeusPatches::ClassMethods extendido na classe Pessoa Module MeusPaches incluso na classe Pessoa => Pessoa >> ?> fabio = Pessoa.new => #<Pessoa:0x1789528> >> fabio.metodo_de_instancia => "sou um metodo de instancia" >> ?> Pessoa.metodo_de_classe => "sou um metodo de classe" |
Você vai entender o que significa “send” na próxima seção. Apenas entenda a seguinte idéia: criamos um módulo chamado “MeusPatches” e um sub-módulo dentro dele chamado “ClassMethods” (poderia ser outro nome, mas só para padronizar). Os métodos do módulo principal ficam disponíveis às instâncias dos objetos e os métodos do sub-módulo ficam disponíveis como métodos de classe. A sequência é assim:
- primeiro definimos o módulo MeusPatches e seu sub-módulo ClassMethods
- definimos a classe Pessoa, uma classe vazia
- incluímos o módulo MeusPatches à classe Pessoa
- o método self.included é ativado que, por sua vez, manda extender o sub-módulo ClassMethods na classe Pessoa (que é passado como o parâmetro “base”)
- agora a classe Pessoa ganhou os métodos de instância e de classe
- criamos a instância “fabio” e chamamos o método “metodo_de_instancia”
- chamamos o método “metodo_de_classe” diretamente da classe Pessoa
Se fôssemos reescrever a classe Pessoa sem o recurso de módulos, ela ficaria assim:
1 2 3 4 5 6 7 8 9 |
class Pessoa def metodo_de_instancia "sou um metodo de instancia" end def self.metodo_de_classe "sou um metodo de classe" end end |
Criar um método a partir de “self” significa que o método está disponível apenas à classe e não às suas instâncias. Novamente, para ser mais fácil de comparar, pense em métodos estáticos de classe como em Java. Mas entenda que não é a mesma coisa: em Java ou C# as classes são estruturas estáticas, em Ruby a própria classe é um objeto (pois ela é instância da classe “Class”). Outra maneira de escrever a mesma coisa seria:
1 2 3 4 5 6 7 8 9 10 11 |
class Pessoa def metodo_de_instancia "sou um metodo de instancia" end class << self def metodo_de_classe "sou um metodo de classe" end end end |
A diferença é que em vez de escrever “def self.” o tempo todo, podemos simplesmente reabrir a metaclasse da classe Pessoa e escrever todos os métodos “localmente” ali dentro. Em ambos os casos a classe vai se comportar da seguinte forma:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
>> p = Pessoa.new => #<Pessoa:0x1775370> >> p.metodo_de_instancia => "sou um metodo de instancia" >> p.metodo_de_classe NoMethodError: undefined method `metodo_de_classe' for #<Pessoa:0x1775370> from (irb):94 >> Pessoa.metodo_de_instancia NoMethodError: undefined method `metodo_de_instancia' for Pessoa:Class from (irb):95 >> Pessoa.metodo_de_classe => "sou um metodo de classe" |
Não perca a Parte II deste artigo para entender o básico da linguagem Ruby. Espero que até aqui tenha ficado bastante claro que não devemos tentar codificar Ruby da mesma forma que codificamos em outras linguagens estáticas. Tirem proveito do dinamismo dessa linguagem, é assim que fazemos o “The Ruby Way”.
Continue lendo a Parte II