09. Clases

Hasta ahora, hemos visto muchos tipos diferentes de objetos, o clases: cadenas, enteros, flotantes, arreglos y algunos objetos especiales (true, false y nil), a los que volveremos más tarde. En Ruby, estas clases siempre comienzan con una letra mayúscula: String, Integer (Enteros), Float (Flotantes), Array (Arreglos), etc. Generalmente, si queremos crear un nuevo objeto de una determinada clase, usamos new:

a = Array.new  + [12345]  # Adición de Arreglos.
b = String.new + 'hola'  # Adición con Cadenas.
c = Time.new

puts 'a = '+a.to_s
puts 'b = '+b.to_s
puts 'c = '+c.to_s

Debido a que podemos crear arreglos y cadenas usando [...] y '...' respectivamente, rara vez usamos new para eso (De cualquier modo, no está muy claro en el ejemplo anterior que String.new crea una cadena vacía y Array.new crea un arreglo vacío). Los números, sin embargo, son una excepción: no puedes crear un entero usando Integer.new. Solo tienes que escribir el número.

La clase Time

Está bien, ¿y la clase Time? Los objetos Time representan momentos en el tiempo. Puedes sumar (o restar) números a (o de) tiempos para obtener nuevos instantes: sumar 1.5 a un tiempo devuelve un nuevo instante un segundo y medio después:

tiempo  = Time.new    # El instante en que cargaste esta página.
tiempo2 = tiempo + 60 # Un minuto después.

puts tiempo
puts tiempo2

También puedes crear un tiempo para un momento específico usando Time.mktime:

puts Time.mktime(2000, 1, 1)          # Año 2000.
puts Time.mktime(1976, 8, 3, 10, 11)  # Año en que nací.

Nota: cuando nací, estaba en uso el Horario de Verano del Pacífico (PDT, en inglés). Cuando llegó el año 2000, sin embargo, estaba en uso el Horario Estándar del Pacífico (PST, en inglés), al menos para nosotros en la costa Oeste. Los paréntesis sirven para agrupar los parámetros para mktime. Cuantos más parámetros añadas, más preciso se volverá tu instante.

Puedes comparar dos tiempos utilizando los métodos de comparación (un tiempo anterior es menor que uno posterior).

Algunas Cosas Para Probar

  • Un billón de segundos... Encuentra el segundo exacto de tu nacimiento (si puedes). Averigua cuándo cumplirás (¿o cuándo cumpliste?) un billón de segundos de edad. Luego ve a marcarlo en tu calendario.
  • ¡Feliz Cumpleaños! Pregunta el año de nacimiento en que nació una persona. Luego pregunta el mes y, finalmente, el día. Luego averigua la edad de esa persona y ¡dale una NALGADA! por cada cumpleaños que haya tenido.

La Clase Hash

Otra clase muy útil es la clase Hash. Los hashes son muy parecidos a los arreglos: tienen un montón de espacios que pueden contener varios objetos. Sin embargo, en un arreglo, los espacios están alineados en una fila, y cada uno está numerado (comenzando desde cero). En un Hash, sin embargo, los espacios no están alineados en una fila (simplemente están todos juntos), y puedes usar cualquier objeto para referirte a un espacio, no solo un número. Es bueno usar hashes cuando tienes un montón de cosas que quieres almacenar, pero que realmente no encajan en una lista ordenada. Por ejemplo, los colores que uso en varias partes de este tutorial:

colorArray = []  # igual que Array.new
colorHash  = {}  # igual que Hash.new

colorArray[0]         = 'rojo'
colorArray[1]         = 'verde'
colorArray[2]         = 'azul'
colorHash['cadenas']  = 'rojo'
colorHash['numeros']  = 'verde'
colorHash['palabras'] = 'azul'

colorArray.each do |color|
  puts color
end
colorHash.each do |tipoCodigo, color|
  puts tipoCodigo + ':  ' + color
end

Si uso un arreglo, tengo que recordar que el espacio 0 es para cadenas, el espacio 1 es para números, etc. Pero si uso un Hash, ¡es fácil! El espacio 'cadenas' almacena el color de las cadenas, por supuesto. Nada que recordar. Es posible que hayas notado que cuando usé each, los objetos en el hash no salieron en el mismo orden en que los puse (Al menos no cuando escribí esto. Tal vez ahora sí... nunca se sabe con los hashes). Los arreglos son para mantener las cosas en orden; los Hashes no.

Aunque la gente generalmente usa cadenas para nombrar los espacios en un hash, puedes usar cualquier tipo de objeto, incluso arreglos y otros hashes (aunque no se me ocurre una razón por la que querrías hacer eso...):

hashRaro = Hash.new

hashRaro[12] = 'monos'
hashRaro[[]] = 'vacío'
hashRaro[Time.new] = 'no hay tiempo como el presente'

Los hashes y los arreglos son buenos para cosas diferentes: depende de ti decidir cuál es mejor para tu problema en particular.

Extendiendo Clases

Al final del último capítulo, escribiste un método para devolver un número en palabras. Sin embargo, ese no era un método de enteros: era solo un método genérico del programa. ¿No sería más genial si pudieras escribir 22.a_palabras en lugar de numeroEspanol 22? Mira cómo puedes hacer eso:

class Integer
  def a_palabras
    if self == 5
      palabras = 'cinco'
    else
      palabras = 'cincuenta y ocho'
    end

    palabras
  end
end

# Prefiero probar siempre en pares...
puts 5.a_palabras
puts 58.a_palabras

Bueno, lo probé; y nada explotó. :)

Definimos un método de entero simplemente "saltando" dentro de la clase Integer, definiendo el método allí y saliendo. Ahora todos los enteros tienen este sensacional (incompleto) método. De hecho, si no te gusta la forma en que el método nativo to_s hace las cosas, puedes simplemente redefinirlo... ¡pero no recomiendo hacer eso! Es mejor dejar los métodos antiguos tranquilos y hacer nuevos cuando necesites algo nuevo.

¿Confundido todavía? Déjame repasar ese último programa un poco más. Hasta ahora, cada vez que ejecutábamos algún código o definíamos un método, lo hacíamos en el objeto "programa" predeterminado. En nuestro último programa, salimos de ese objeto por primera vez y entramos en la clase Integer. Definimos un método allí (lo que lo convirtió en un método de entero) y todos los enteros pueden usarlo. Dentro de ese método, usamos self para referirnos al objeto (el entero) que está usando el método.

Creando Clases

Ya hemos visto un montón de objetos de clases diferentes. Sin embargo, es fácil crear tipos de objetos que Ruby no tiene. Por suerte, crear una clase nueva es tan fácil como extender una existente. Supongamos que queremos lanzar algunos dados en Ruby. Mira cómo podemos hacer una clase llamada Dado:

class Dado

  def rodar
    1 + rand(6)
  end

end

# Vamos a hacer dos dados...
dados = [Dado.new, Dado.new]

# ...y rodar cada uno de ellos.
dados.each do |dado|
  puts dado.rodar
end

(Si te saltaste la sección sobre números aleatorios, rand(6) simplemente devuelve un número aleatorio entre 0 y 5).

¡Eso es todo! Objetos de nuestra propia creación. Rueda los dados algunas veces (usando el botón de "Actualizar" de tu navegador) y mira qué sucede.

Podemos definir todo tipo de métodos para nuestros objetos... pero falta algo. Trabajar con estos objetos no ha cambiado mucho desde que aprendimos sobre variables. Mira nuestro dado, por ejemplo. Cada vez que lo rodamos, obtenemos un número diferente. Pero si quisiéramos guardar ese número, tendríamos que crear una variable para apuntar a él. Y cualquier dado decente debería tener un número, y rodar el dado debería cambiar ese número. Si guardamos el dado, no tenemos forma de saber qué número está mostrando.

Sin embargo, si intentamos almacenar el número que sacamos en una variable (local) dentro de rodar, el valor se perderá tan pronto como rodar termine. Necesitamos guardar ese número en un tipo diferente de variable:

Variables de Instancia

Normalmente cuando hablamos sobre cadenas, simplemente las llamamos cadenas. Sin embargo, podríamos llamarlas Objetos de tipo Cadena. A veces, algunos programadores pueden llamarlas instancias de la clase String, pero esa es una forma exagerada (y muy larga) de decir cadena. Una instancia de una clase es simplemente un objeto de esa clase.

Por lo tanto, las variables de instancia son como variables de objeto. Una variable local de un método vive hasta que el método termina. Una variable de instancia de un objeto, por otro lado, vivirá mientras el objeto esté vivo. Para diferenciar las variables de instancia de las variables locales, tienen una @ delante de sus nombres:

class Dado

  def rodar
    @numeroMostrado = 1 + rand(6)
  end

  def mostrado
    @numeroMostrado
  end

end

dado = Dado.new
dado.rodar
puts dado.mostrado
puts dado.mostrado
dado.rodar
puts dado.mostrado
puts dado.mostrado

¡Muy bien! Ahora rodar rueda el dado y mostrado nos dice qué número salió. ¿Pero qué pasa si queremos ver qué número salió antes de rodar el dado (antes de haber definido @numeroMostrado)?

class Dado

  def rodar
    @numeroMostrado = 1 + rand(6)
  end

  def mostrado
    @numeroMostrado
  end

end

# Ya que no voy a usar este dado de nuevo,
# no necesito guardarlo en una variable.
puts Dado.new.mostrado

Mmm... Bueno, al menos no nos dio un error. Espera, no tiene mucho sentido un dado "no rodado" o lo que sea que nil signifique aquí. Sería mucho más genial si pudiéramos rodar el dado tan pronto como se crea. Para eso es initialize:

class Dado

  def initialize
    # Solo voy a rodar el dado, aunque podríamos hacer
    # cualquier cosa que queramos, como poner la cara '6'
    # hacia arriba
    rodar
  end

  def rodar
    @numeroMostrado = 1 + rand(6)
  end

  def mostrado
    @numeroMostrado
  end

end

puts Dado.new.mostrado

Cuando se crea un objeto, el método initialize (si está definido) siempre se llama.

Nuestro dado es casi perfecto. La única cosa que falta es una manera de establecer qué número se está mostrando... ¿Por qué no escribes el método trampa que haga eso? Vuelve cuando hayas terminado (y cuando lo hayas probado y funcione, por supuesto). ¡Solo asegúrate de que nadie pueda hacer que el dado muestre un 7!

Fue muy genial lo que hicimos hasta ahora. Pero fue solo un juego, aun así. Déjame mostrarte un ejemplo más interesante. Vamos a hacer una mascota virtual, un bebé dragón. Al igual que todos los bebés, debe poder comer, dormir y "atender a la naturaleza", lo que significa que vamos a tener que poder alimentarlo, ponerlo a dormir y llevarlo al patio. Internamente, nuestro dragón necesita saber si tiene hambre, está cansado o si necesita salir, pero no podremos ver eso mientras estemos interactuando con él, al igual que no puedes preguntarle a un bebé "¿tienes hambre?". Así que vamos a agregar algunas formas divertidas de interactuar con nuestro bebé dragón, y cuando nazca le daremos un nombre (Cualquier cosa que pases como parámetro al método new se pasará al método initialize para ti). Bien, intentémoslo:

class Dragon

  def initialize nome
    @nombre = nome
    @dormido = false
    @comidaEstomago  = 10 # Está lleno
    @comidaIntestino =  0 # No necesita ir al patio

    puts @nombre + ' nació.'
  end

  def alimentar
    puts 'Alimentaste a ' + @nombre + '.'
    @comidaEstomago = 10
    pasoDelTiempo
  end

  def patio
    puts 'Llevaste a ' + @nombre + ' al patio.'
    @comidaIntestino = 0
    pasoDelTiempo
  end

  def ponerEnCama
    puts 'Pusiste a ' + @nombre + ' en la cama.'
    @dormido = true
    3.times do
      if @dormido
        pasoDelTiempo
      end
      if @dormido
        puts @nombre + ' está roncando y llenando la habitación de humo.'
      end
    end
    if @dormido
      @dormido = false
      puts @nombre + ' se está despertando.'
    end
  end

  def jugar
    puts 'Lanzas a ' + @nombre + ' al aire.'
    puts 'Él se ríe y te quema las cejas.'
    pasoDelTiempo
  end

  def mecer
    puts 'Meces a ' + @nombre + ' suavemente.'
    @dormido = true
    puts 'Comienza a dormitar...'
    pasoDelTiempo
    if @dormido
      @dormido = false
      puts '...pero se despierta cuando paras.'
    end
  end

  private

  # "private" significa que los métodos definidos aquí
  # son métodos internos del objeto. (Puedes
  # alimentarlo, pero no puedes preguntarle si
  # tiene hambre.)

  def conHambre?
    # Los nombres de métodos pueden terminar con "?".
    # Normalmente, hacemos esto solo
    # si el método devuelve verdadero o falso,
    # como este:
    @comidaEstomago <= 2
  end

  def necesitaSalir?
    @comidaIntestino >= 8
  end

  def pasoDelTiempo
    if @comidaEstomago > 0
      # Mover la comida del estómago al intestino.
      @comidaEstomago  = @comidaEstomago  - 1
      @comidaIntestino = @comidaIntestino + 1
    else  # ¡Nuestro dragón está hambriento!
      if @dormido
        @dormido = false
        puts '¡Se está despertando!'
      end
      puts '¡' + @nombre + ' está hambriento! ¡En su desesperación, te comió a TI!'
      exit  # Esto sale del programa.
    end

    if @comidaIntestino >= 10
      @comidaIntestino = 0
      puts '¡Uy!  ' + @nombre + ' tuvo un accidente...'
    end

    if conHambre?
      if @dormido
        @dormido = false
        puts '¡Se está despertando!'
      end
      puts 'El estómago de ' + @nombre + ' está rugiendo...'
    end

    if necesitaSalir?
      if @dormido
        @dormido = false
        puts '¡Se está despertando!'
      end
      puts @nombre + ' hace el baile para ir al patio...'
    end
  end

end

mascota = Dragon.new 'Norbert'
mascota.alimentar
mascota.jugar
mascota.patio
mascota.ponerEnCama
mascota.mecer
mascota.ponerEnCama
mascota.ponerEnCama
mascota.ponerEnCama
mascota.ponerEnCama

¡GUAU! Claro que sería mucho más genial si este fuera un programa interactivo, pero puedes hacer esa parte después. Solo estaba tratando de mostrarte las partes relacionadas directamente con crear una nueva clase del tipo Dragon.

Dijimos un montón de cosas nuevas en este ejemplo. La primera es simple: exit termina el programa donde esté. La segunda es la palabra private, que pusimos justo en medio de nuestra clase. Podría haberla dejado fuera, pero solo quería reforzar la idea de que ciertos métodos podías hacerlos con un dragón, mientras que otros sucedían con el dragón. Puedes pensar en esto como "cosas bajo el capó": a menos que seas un mecánico de automóviles, todo lo que necesitas saber sobre autos es el acelerador, el freno y la dirección. Un programador llama a esto interfaz pública.

Ahora, para un ejemplo más concreto en esta línea de razonamiento, hablemos un poco sobre cómo representarías un auto en un juego (que es mi línea de trabajo). Primero, necesitas decidir cómo se verá tu interfaz pública; en otras palabras, ¿qué métodos pueden llamar las personas de tus objetos tipo auto? Bueno, deben poder acelerar y frenar, pero también necesitan poder definir la fuerza que están aplicando en el pedal (Hay una gran diferencia entre tocar el acelerador y hundir el pie). También necesitarán conducir, y de nuevo, decir qué fuerza están aplicando en la dirección. Creo que puedes ir aún más lejos y agregar un embrague, intermitentes, lanzacohetes, incinerador trasero, un condensador de flujo, etc... depende del tipo de juego que estés haciendo.

Los objetos internos a un auto, sin embargo, son más complejos: otras cosas que un auto necesita son la velocidad, la dirección y la posición (quedándonos en lo básico). Estos atributos serán modificados presionando el pedal del acelerador o el de freno y girando el volante, claro, pero el usuario no debe poder alterar esa información directamente (lo que sería una distorsión). Es posible que desees verificar el derrape o el daño, la resistencia del aire y así sucesivamente. Todo esto concierne solo al auto. Todo esto es interno al auto.

Algunas Cosas Para Probar

  • Haz una clase de Naranjo. Debe tener un método altura que devuelva su altura, un método llamado pasar_un_anio que, cuando se llame, haga que el árbol complete un año más de vida. Cada año, el árbol crece más (no importa cuán grande creas que un naranjo pueda crecer en un año), y después de algunos años (nuevamente, tú decides) el árbol debe morir. En los primeros años, no debe producir frutos, pero después de un tiempo sí, y creo que los árboles más viejos producen muchos más frutos que uno más joven con el paso de los años... o lo que te parezca más lógico. Y, por supuesto, debes poder contar_las_naranjas (el número de naranjas en el árbol), y tomar_una_naranja (que reducirá el @numero_de_naranjas en uno y devolverá una cadena diciendo cuán deliciosa estaba la naranja, o si no dirá que no hay más naranjas este año). Recuerda que las naranjas que no tomes este año deben caer antes del próximo año.
  • Escribe un programa para que puedas interactuar con tu bebé dragón. Debes poder ingresar comandos como alimentar y patio, y esos métodos deben llamarse en tu dragón. Lógicamente, como toda la entrada será por cadenas, debes tener una forma de despachar los métodos, donde tu programa debe validar la cadena ingresada y llamar al método apropiado.

¡Y eso es todo! Pero espera un poco... ¡No te dije nada sobre clases para hacer cosas como enviar un correo electrónico, o guardar y cargar archivos de tu computadora, o cómo crear ventanas y botones, o mundos en 3D o cualquier cosa! Bueno, hay demasiadas clases que puedes usar, y eso hace imposible que te las muestre todas; incluso yo no las conozco todas. Lo que puedo decirte es dónde encontrar más sobre ellas, para que puedas aprender más sobre las que quieras usar. Pero antes de dejarte ir, hay una característica más de Ruby que deberías conocer, algo que la mayoría de los otros lenguajes no tienen, pero sin lo que simplemente no puedo vivir: bloques y procs.