Orientação a Objetos com Ruby
Veja neste artigo como codificar orientado a objetos no Ruby. Veremos as definições de classes, atributos, métodos, instância, entre outros.
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. Além disso, ela não admite a repetição de código, sendo uma das suas principais características a reutilização de código.
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/.
Artigos relacionados
-
Artigo
-
Artigo
-
Artigo
-
Artigo
-
Artigo