A orientação a objetos é um paradigma de programação que considera elementos do mundo real através de objetos, onde cada um é único, de acordo com as suas características.

Assim, o Ruby é considerado uma linguagem puramente orientada a objetos, visto que tudo é considerado um objeto, até mesmo os tipos básicos da linguagem.

Por isso, no restante do artigo veremos alguns exemplos práticos de como programar orientado a objeto no Ruby.

Antes de falarmos sobre orientação a objetos devemos comentar sobre a definição e uso de métodos no Ruby, que não precisam estar dentro da definição de uma classe.

Método

Os métodos começam com uma definição, um nome, parâmetros, o corpo e o marcador final da definição, como mostra o exemplo da Listagem 1.

Listagem 1. Exemplo de método no Ruby.

  def exibe_soma(arg1, arg2)
     print arg1 + arg2
  end

Também é possível utilizar a variável dentro do corpo do método como no exemplo da Listagem 2.

Listagem 2. Utilizando variáveis no corpo do método.

  def usar_farois(tipo_brilho)
     puts "Acendendo farol #"
  end

O nome do método pode conter letras minúsculas separadas por underscores, que é uma boa prática, além de números, que são raramente utilizados. Também é permitido que o nome termine com um ponto de interrogação (?), ponto de exclamação (!) ou sinal de igual (=). Essas terminações não têm nenhum significado especial para Ruby, mas há certas convenções em torno de seu uso, já que essa nomenclatura é mais usada em nomes de variáveis. Outra boa prática da indústria de software que utiliza Ruby é nunca utilizar parênteses após a definição do método, apesar de ser permitido, conforme o exemplo da Listagem 3.

Listagem 3. Utilizando parênteses na definição do método.

  def no_args()
   puts "Método com parênteses!"
  end 

Chamando um método

Para chamar os métodos que foram definidos podemos usar os comando da Listagem 4, onde temos um simulador de veículos que contém basicamente três métodos e as suas respectivas chamadas.

Listagem 4. Exemplo de definição de métodos e chamadas.

def acelerar
 puts "Pisando no acelerador"
 puts "Acelerando…"
end
   
def buzinar
 puts "Pressionando a buzina"
 puts "Beep beep!"
end
   
def usar_farois(tipo_farol)
 puts "Acendendo #"
end
   
buzinar
acelerar
usar_farois("farol-baixo")

O Ruby permite que as chamadas de métodos sejam colocadas em qualquer lugar, como no exemplo apresentado, em que as chamadas foram colocadas imediatamente após a definição dos métodos. Outra observação interessante é que para executar os métodos não precisamos chamar nenhum objeto, como no Java, que utiliza, por exemplo, a chamada objeto.metodo(). Isso ocorre porque métodos que estão definidos fora de qualquer classe estão incluídos no nível mais alto do ambiente de execução, assim como puts e print.

Cada método têm seus parâmetros, ou seja, suas características. Aprenderemos mais como utilizá-las na próxima seção.

Parâmetros

Para passar informações a um método pode-se incluir um ou mais parâmetros depois do seu nome, separados por vírgulas. Nesse caso, os parênteses devem ser utilizados, apesar de não ser obrigatório, mas a boa prática pede a sua utilização. Dentro do corpo do método essas variáveis podem ser utilizadas assim como ocorre com quaisquer delas, como no exemplo da Listagem 5.

Listagem 5. Utilizando mais variáveis como parâmetro no método.

  def imprime_area(largura, altura)
    puts largura * altura
  end

Às vezes têm-se alguns métodos que são utilizados com o mesmo parâmetro em 90% das vezes e precisamos repeti-los a todo o momento. Nesse caso, o Ruby ajuda os desenvolvedores através dos parâmetros opcionais. Assim, basta definir um valor padrão na declaração do método, como mostra o exemplo da Listagem 6.

Listagem 6. Parâmetros opcionais

usar_farois("farol-baixo")
  parar_motor
  comprar_cafe
  iniciar_motor
  usar_farois("farol-baixo")
  acelerar

Pode-se notar que o método usar_farois("farol-baixo") está sendo usado repetidas vezes com o mesmo parâmetro. Para contornar este problema bastaria fazer conforme a Listagem 7.

Listagem 7. Evitando repetições utilizando parâmetros com valores padrão.

  def usar_farois(tipo_farol="farol-baixo")
    puts "Acendendo #"
  end

Se for necessário fornecer outro tipo de parâmetro basta passar o valor desejado, porém, se não precisar alterar o valor padrão basta chamar o método normalmente sem nenhum parâmetro. A seguir temos um exemplo utilizando o valor padrão e um valor definido, respectivamente:

usar_farois
usar_farois("farol-alto")

Retorno do método

Os métodos no Ruby também permitem o retorno de valores, ou seja, um valor que pode ser enviado de volta para o código que chamou o método. Isso é realizado utilizando a palavra-chave return, conforme o exemplo a seguir:

  def media(quilometragem_dirigida, gasolina_usada)
    return quilometragem_dirigida / gasolina_usada
  end

Para chamar o método utiliza-se o código a seguir:

media_viagem = media (400, 12)
puts "Média da viagem foi de #."

Porém, o Ruby também permite o retorno implícito, ou seja, não é necessário utilizar o return:

  def media(quilometragem_dirigida, gasolina_usada)
   quilometragem_dirigida / gasolina_usada
  end

Alguns desenvolvedores se perguntam então por que o Ruby possui a palavra reservada return se ela não é necessária para retornar um valor. A linguagem mantém essa palavra reservada porque o return causa a saída do método sem precisar executar as próximas linhas de código. Na Listagem 8 temos um exemplo dessa situação em que o return seria útil.

Listagem 8. Método com retorno para evitar execução posterior de código.

  def media(quilometragem_dirigida, gasolina_usada)
    if gasolina_usada == 0
      return 0.0
    end
   
   quilometragem_dirigida / gasolina_usada
  end
  

Nesse caso a divisão nunca seria realizada se gasolina_usada fosse zero, evitando, inclusive, uma exceção de divisão por zero.

De forma geral, os métodos são muito importantes para reduzir a duplicação de código e assim mantê-lo organizado.

Na próxima seção entenderemos como juntar esses métodos em uma classe.

Definindo classes

O benefício de utilizar os objetos está em manter o conjunto de informações e os métodos que operam sobre essas informações em um mesmo lugar. Para isso é necessário utilizar as classes, que descrevem o que o objeto sabe (através dos seus atributos) e como faz (através dos seus métodos).

A instância é um objeto que foi criado através de uma classe. Por isso, os desenvolvedores escrevem apenas uma classe e com isso pode-se criar múltiplas instâncias dela. Esse conceito não pode ser confundido com os conceitos de variáveis de instância e métodos de instância (ou métodos de classe).

As variáveis de instância são aquelas que pertencem a um objeto e representam o estado dele (suas informações), além de possuírem diferentes valores para cada instância da classe. Já os métodos de instância são aqueles que podem ser chamados diretamente no objeto, ou seja, não é necessário criar uma instância da classe que o método pertence para então invocá-lo.

Cada instância de uma classe tem seus próprios valores que são usados pelos seus métodos. Na Listagem 9 temos um exemplo da classe no Ruby com seus respectivos métodos.

Listagem 9. Exemplo de classe no Ruby.

class Cachorro
   
 def falar
  puts "Latir!"
 end
   
 def mover(destino)
   puts "Correndo para o #."
 end
   
end  

No código tem-se a palavra-chave "class", que inicia a definição de uma classe, seguida pelo nome da mesma. Dentro da definição tem-se as pertencentes aos métodos, onde a classe é marcada com "end" para delimitar o fim da sua definição.

Para criar instâncias da classe utilizamos o código a seguir:

  snoopy = Cachorro.new
  rex = Cachorro.new

Em ambas as linhas foram criadas uma instância de uma classe e atribuído a uma variável. Agora já é permitido chamar os métodos da classe conforme o código a seguir:

  snoopy.falar
  rex.mover("tigela de comida")

O orientação a objetos ajuda na organização do código: uma situação muito comum é quando se têm várias condicionais ifs, conforme o código da Listagem 10.

Listagem 10. Diversas condicionais para cada tipo.

  def falar(tipo_animal, nome)
      if tipo_animal == "passaro"
        puts "# canta!"
      elsif tipo_animal == "cachorro"
        puts "# late!"
       elsif tipo_animal == "gato"
          puts "# mia!"
       end
  end

Quanto mais animais surgirem, maior ficará a quantidade de condicionais nesse código. Porém, utilizando as classes poderíamos codificar o mesmo código separando cada parte, conforme a Listagem 11.

Listagem 11. Utilizando classes para definir os tipos.

class Passaro
  def falar
    puts "Canta!"
  end
   
  def mover(destino)
    puts "Voando para #."
  end
end
   
class Cachorro
  def falar
    puts "Late!"
  end
   
  def mover(destino)
   puts "Correndo para #."
  end
end
   
class Gato
  def falar
    puts "Mia!"
  end
   
  def mover(destino)
    puts "Correndo para #."
   end
end 

Na abordagem orientada a objetos, cada classe representa um tipo de animal. Assim, ao invés de um método enorme para todos os tipos de animais, coloca-se um pequeno método para cada classe, em que cada um define um comportamento específico para cada tipo de animal.

Variável de instância e método de acesso

Um objeto também pode armazenar as variáveis de instância, que são visíveis em toda a classe e não somente no escopo local de um método. Elas permanecem ativas até o final da vida de um objeto e são declaradas como variável comum, porém o nome deve começar com o símbolo arroba (@), conforme mostra o código da Listagem 12.

Listagem 12. Declarando variáveis de instância no Ruby.

class Cachorro
 def setar_nome
   @nome = "Rex"
 end
   
 def falar
   puts "#{@nome} Late!"
  end
end  

A variável “nome”, que deveria ser de escopo local do método e, portanto, apenas utilizada dentro dele, agora torna-se uma variável de instância que pode ser utilizada em toda a classe.

Para alterar a variável de instância poderíamos pensar em utilizar um código como "rex.@idade = 3", porém o Ruby não aceita isso como uma sintaxe válida, forçando os desenvolvedores a utilizarem o conceito de encapsulamento, prevenindo o acesso direto de outras partes do programa.

Dessa forma, é preciso criar métodos de acesso, que podem ser de dois tipos para cada variável de instância: um método de leitura e um método de escrita, conforme mostra o exemplo da Listagem 13.

Listagem 13. Definindo métodos de acesso no Ruby.

  class MinhaClasse
    def meu_atributo=(novo_valor)
     @meu_atributo = novo_valor
    end
   
    def meu_atributo
     @meu_atributo
    end
  end

O primeiro método é um método de escrita e o segundo é um método de leitura e os dois são chamados de métodos de acesso.

Para utilizar os métodos primeiramente instancia-se a classe através do código "minha_instancia = MinhaClasse.new", posteriormente pode-se configurar o atributo através do código minha_instancia.meu_atributo = "valor" ou então lê-lo utilizando "puts minha_instancia.meu_atributo". A chamada minha_instancia.meu_atributo = ("valor") também é permitida no Ruby.

Não podemos esquecer que a linguagem preza pela produtividade, por isso a forma apresentada anteriormente para definição dos métodos de acesso pode ser simplificada. Para isso basta utilizar o atalho oferecido pela mesma para criar os métodos de acesso através dos nomes de métodos attr_writer, attr_reader, e attr_accessor. Chamando-os dentro da classe, automaticamente serão criados os métodos de acesso.

Por exemplo, dado o método de escrita abaixo para a variável de instância nome:

  def nome=(novo_valor)
   @nome = novo_valor
  end

Podemos simplesmente utilizar o código abaixo, que o mesmo efeito seria produzido:

attr_writer :nome

Dado o método de leitura a seguir:

  def name
   @name
  end

Bastaria utilizar o método abaixo:

attr_reader :nome

Para criar automaticamente os dois métodos de acesso ao mesmo tempo bastaria ter utilizado o código a seguir:

attr_accessor :nome

Para definir métodos de acesso para mais de uma variável de instância pode-se utiliza o código abaixo:

attr_accessor :nome, :idade

No entanto, não podemos esquecer que os métodos de acesso também tem o propósito de proteger as informações, principalmente no método de escrita que é responsável por configurar o valor a ser armazenado.

Para isso deve-se definir um attr_reader :nome, :idade para criar um método de acesso para leitura e assim definiríamos o método de escrita manualmente, conforme a Listagem 14.

Listagem 14. Definindo o método de escrita manualmente.

  def nome=(valor)
    if valor == ""
      puts "O Nome não pode estar em branco!"
    else
     @nome = valor
    end
  end
   
  def idade=(valor)
   if valor < 0
    puts "A idade # não é válida!"
    else
     @idade = valor
    end
  end

No código apresentado as variáveis estão protegidas dos valores em branco para os nomes e números negativos para a idade.

Outra possibilidade mais elegante e recomendada pela comunidade, é utilizar o método "raise" para interromper a execução do programa e enviar uma mensagem ao usuário, conforme o exemplo da Listagem 15.

Listagem 15. Utilizandoraisepara interromper a execução.

  class Cachorro
    attr_reader :nome, :idade
   
    def nome=(valor)
      if valor == ""
       raise "O Nome não estar em branco!"
      end
      @nome = valor
    end
   
    def idade=(valor)
      if valor < 0
        raise "A idade # não é válida!"
      end
      @idade = valor
    end
   
    def relatorio
      puts "#{@nome} está com #{@idade} anos de idade."
    end
  end 

Se for passada uma idade negativa teríamos o seguinte erro:

A idade -1 não é válida! (RuntimeError)

Herança

Assim como outras linguagens orientadas a objetos, o Ruby também oferece a possibilidade de herdar o comportamento de outras classes.

Dessa forma, o desenvolvedor, ao invés de repetir a definição de métodos por classes similares, pode realizar essa operação em uma única classe (também chamada de superclasse) e as outras que possuem métodos comuns (chamadas de subclasses) herdam essas funcionalidades da sua superclasse. A herança ajuda a reduzir substancialmente a duplicação de código.

As subclasses herdam os atributos, métodos e métodos de acesso, mas não herdam variáveis de instância.

Segue na Listagem 16 um exemplo de uma superclasse no Ruby.

Listagem 16. Exemplo de superclasse no Ruby.

  class Veiculo
   attr_accessor :odometro
   attr_accessor :gasolina_usada
   
   def acelerar
     puts "Acelerar!"
   end
   
   def buzinar
    puts "Beep! Beep!"
   end
   
   def dirigir
     puts "Ligar 2 rodas dianteiras."
   end
   
   def media
    return @odometro / @gasolina_usada
   end
  end

Para herdar dessa superclasse pode-se utilizar o código abaixo:

  class Carro < Veiculo
  end 

O símbolo "menor que" (<) é utilizado para definir que a subclasse é um subconjunto da superclasse. Assim, a subclasse Carro herda todos os atributos e métodos de Veiculo.

Segue na Listagem 17 um exemplo de como utilizar a subclasse

Listagem 17. Subclasse

  carro = Carro.new
  carro.odometer = 11432
  carro.gasolina_usada = 366
  puts "Média:"
  puts carro.media

Outros métodos específicos do Carro podem ser adicionados à subclasse Carro e chamados da mesma forma apresentada.

Se o método da superclasse não tem exatamente o comportamento que se esperava para a sua subclasse, podemos usar o mecanismo de sobrescrita de método (ou overriding) para sobrescrever o comportamento da superclasse. Assim, estamos substituindo o comportamento do método herdado com um método mais específico para subclasse.

Na Listagem 18 temos um exemplo da classe Moto substituindo o método dirigir da classe Veiculo:

Listagem 18. Sobrescrevendo um método.

  class Moto < Veiculo
    def dirigir
      puts "Ligar a roda dianteira."
    end
  end

Agora, chamando o método dirigir, a partir do objeto Moto, tem-se como resultado a execução do método sobrescrito pela classe Moto:

moto.dirigir

Porém, se chamarmos qualquer outro método tem-se como resultado o método definido na superclasse. Por exemplo, se chamarmos moto.acelerar tem-se como resultado "Acelerar!".

Existem algumas situações em que queremos sobrescrever um método e adicionar mais algumas funcionalidades. Para isso existe o "super", que chama o método da superclasse, conforme o exemplo da Listagem 19.

Listagem 19. Chamando o método da superclasse comsuper.

  class Pessoa
   def cumprimento
    puts "Olá!"
   end
  end
   
  class Amigo < Pessoa
   def cumprimento
     super
     puts "Feliz em te ver!"
   end
  end

Nesse caso, a classe Amigo chama a funcionalidade do método cumprimento da superclasse e adiciona o "puts" na sua própria subclasse. Para testar basta executar a chamada abaixo:

Amigo.new.cumprimento

E a saída será:

  Olá!
  Feliz em te ver!

Outra forma de escrever o mesmo código seria conforme a Listagem 20.

Listagem 20. Utilizando uma variável auxiliar para armazenar o valor do método da superclasse.

  class Pessoa
    def cumprimento
     "Olá!"
    end
  end
   
  class Amigo < Pessoa
    def cumprimento
     cumprimento_basico = super
     "# Feliz em te ver!"
   end
  end

Chamando o código através da invocação "puts Amigo.new.cumprimento" tem-se como saída a mesma que a apresentada anteriormente. A diferença é que agora a variável cumprimento_basico recebe o retorno do método "cumprimento" definido na superclasse.

Também é possível passar argumentos para o método na superclasse usando o super, conforme o exemplo da Listagem 21.

Listagem 21. Passando parâmetros para o método da superclasse.

  class Pessoa
   def cumprimento_pelo_nome(nome)
    "Olá, #!"
   end
  end
   
  class Amigo < Pessoa
   def cumprimento_pelo_nome(nome)
     cumprimento_basico = super(nome)
     "# Feliz em te ver!"
   end
  end

Assim pode-se chamar o método através do código:

puts Amigo.new.cumprimento_pelo_nome("Daniel")

E a saída seria "Olá, Daniel! Feliz em te ver!".

Outra forma ainda seria não passar nenhum parâmetro, assim o que for recebido como parâmetro no método sobrescrito é o que será passado como parâmetro para a superclasse.

NOTA: Devemos atentar que super e super() não são a mesma coisa, pois super chama o método sobrescrito com o mesmo argumento que o método recebeu, enquanto o super() chama o método sobrescrito sem nenhum argumento.

Todas as classes no Ruby herdam, por padrão, a classe Object. Para saber a superclasse de uma classe basta executar o comando NomeDaClasse.superclass. Assim, se nenhuma superclasse for definida, o Ruby implicitamente configura Object como superclasse. Isso ocorre pois essa classe possui diversos métodos úteis necessários para os objetos do Ruby como, por exemplo:

  • "to_s" - converte um objeto emstring;
  • "inspect" - converte um objeto emstring de debug;
  • "methods" - retorna os métodos que pertencem ao objeto.

Uma boa prática é que as subclasses sempre sobrescrevam o método "to_s", que, por padrão, retorna uma mensagem pouco amigável para o usuário. Na Listagem 22 temos um exemplo de sobrescrita deste método:

Listagem 22. Sobrescrevendo o métodoto_sda classeObject.

  class Cachorro < Animal
    def to_s
     "nome #{@nome}, idade #"
     end
  end

OBS: Sobrescrever este método também ajuda no debugging do código.

Initialize

Para inicializar as propriedades de uma classe podemos utilizar o método initialize, que é chamado antes de qualquer método, conforme o exemplo da Listagem 23.

Listagem 23. Inicializando valores da classe.

  class MinhaClasse
    def initialize
      puts "Configurando uma nova instância!"
     end
  end

Assim, quando a classe for instanciada através do MinhaClasse.new, o método imediatamente será executado e imprime na tela "Configurando uma nova instância!". Este método é útil para inicializar valores numerais, por exemplo, pois tudo no Ruby é nil (nulo) por padrão e, se for necessária realizar uma operação matemática em alguma variável que ainda não tenha recebido um valor, pode-se ter uma exceção gerada.

O initialize também permite receber valores, conforme o código da Listagem 24.

Listagem 24. Definindo parâmetros para o métodoinitialize.

class MinhaClasse
 def initialize(meu_parametro)
   puts "Parâmetro recebido: #"
 end
end

Assim, pode-se chamar a classe conforme o código MinhaClasse.new("Teste Parâmetro").

Outro artifício muito utilizado é inicializar, por padrão, os valores diretamente no método initialize, como mostra o exemplo da Listagem 25.

Listagem 25. Definindo valores padrão para os parâmetros do métodoinitialize.

def initialize(nome = "Anônimo", salario = 0.0)
  @nome = nome
  @salario = salario
end 

No entanto, o grande problema de fazer isso é que o usuário poderia entrar com qualquer valor para essas variáveis, ou seja, as validações que estão nos métodos de acesso teriam que ser duplicadas.

Para solucionar, oRuby fornece o self,que refere-se ao objeto atual, como é o this do Java.

Assim, para resolver o problema da Listagem 25 pode-se utilizar o código da Listagem 26.

Listagem 26. Utilizando self para chamar os métodos de acesso das instâncias.

class Empregado
  ...
  def initialize(nome = "Anônimo", salario = 0.0)
   self.nome = nome
   self.salario = salario
  end
...
end 

Agora os métodos de acesso serão invocados normalmente.

Método de classe

Por fim, o Ruby também suporta os métodos de classe, que podem ser invocados diretamente na classe, sem precisar instanciá-la. A definição é semelhante à de um método, como mostra a Listagem 27.

Listagem 27. Definindo métodos de classe no Ruby.

class MinhaClasse
   def MinhaClasse.metodo_de_classe(p1, p2)
     puts "Teste de método de classe: #, #"
   end
end

Pode-se notar que na definição do método tem-se o nome da classe seguido de ponto e o nome. Outra possibilidade seria utiliza o def self.metodo_de_classe(p1, p2).

Para invocar este método basta utilizar o código MinhaClasse.metodo_de_classe(1, 2).

Vimos nos exemplos deste artigo que a linguagem Ruby é puramente orientada a objetos e, como é interpretada, as definições dos programas são executados, sem necessidade de recompilar.

Espero que tenham gostado e até a próxima.

Bibliografia

[1] Jay McGavren. Head First Ruby (O’Reilly, 2015).

[2] Ruby Programming Language. Disponível em https://www.ruby-lang.org/.