Super en Ruby

12 Aug
Published by ApuX in

Tags 

Ruby

Super

Métodos en subclases y superclases

Al momento de ejecutar un método de un objeto en Ruby, el interprete busca ese método en la definición de la clase, si lo encuentra, lo ejecuta, y si no, lo busca en la superclase. Ejemplo:

class Persona
  def dormir
    puts "Durmiendo..."
  end
 
  def comer
    puts "Comiendo..."
  end
 
  def correr
    puts "Corriendo..."
  end
end
 
class Empleado < Persona
  def trabajar
    puts "Trabajando..."
  end
end

Su ejecución:

empleado = Empleado.new
empleado.trabajar
# => trabajando...
empleado.dormir
# => durmiendo...

Ambos métodos fueron ejecutados, sólo que trabajar fue tomado la la clase Empleado mientras que dormir fue tomado de la clase Persona.

Cuando se sobrescribe un método, el método de la superclase queda oculto y solo se ejecuta el de la subclase.

class Persona
  def dormir
    puts "Durmiendo..."
  end
 
  def comer
    puts "Comiendo..."
  end
 
  def correr
    puts "Corriendo..."
  end
end
 
class Empleado < Persona
  def trabajar
    puts "Trabajando..."
  end
 
  def dormir
    puts 'durmiendo en el trabajo.'
  end
end

Aquí, el método trabajar es declarado dentro de la clase Persona, pero también es declarado en la clase Empleado, por lo que, cuando se manda a ejecutar desde el objeto empleado, el que se ejecuta es el método de de esta última.

empleado = Empleado.new
empleado.dormir
# => durmiendo en el trabajo.

Llamado al método de una superclase desde una subclase

Sin embargo, es probable que el método dormir de la subclase requiera utilizar funcionalidad del método pero en la superclase. Para este caso, no podemos llamar dormir directamente porque eso generaría un llamado recursivo sobre sí mismo. Ejemplo:

class Empleado < Persona
  def trabajar
    puts "Trabajando..."
  end
 
  def dormir
    dormir # intenta llamar al método dormir de la superclase
    puts "en el trabajo"
  end
end

Al tratar de ejecutar el método dormir devolvería un error stack level too deep (SystemStackError). En esos casos, se debe utilizar la palabra reservada super.

class Empleado < Persona
  def trabajar
    puts "Trabajando..."
  end
 
  def dormir
    super # intenta llamar al método dormir de la superclase
  puts "en el trabajo"
end
end

Con esto, el método dormir de la clase Empleado puede utilizar la funcionalidad del método dormir de la clase Persona.

empleado = Empleado.new
empleado.dormir
# => Durmiendo...
# => en el trabajo

Parámetros para super

Ahora, este método tiene un comportamiento particular que es necesario conocer para no incurrir en errores.

Si el método en la clase padre recibe parámetros, es posible llamarlo especificando los parámetros en la llamada, o bien, sin especificar ningún parámetro. En el siguiente ejemplo vemos ambos comportamientos, los métodos dormir y jugar reciben dos parámetros tanto en la superclase como la subclase, pero la llamada a super del primer método se hace con parámetros (línea 25) y en el segundo método se hace sin parámetros (línea 30). Ambos funcionan correctamente.

class Persona
  def dormir(numero_de_horas, numero_de_minutos)
    puts "Durmiendo... por #{numero_de_horas} horas y #{numero_de_minutos} minutos"
  end
 
  def comer
    puts "Comiendo..."
  end
 
  def correr
    puts "Corriendo..."
  end
 
  def jugar(numero_de_horas, numero_de_minutos)
    puts "Jugando... por #{numero_de_horas} horas y #{numero_de_minutos} minutos"
  end
end
 
class Empleado < Persona
  def trabajar
    puts "Trabajando..."
  end
 
  def dormir(numero_de_horas, numero_de_minutos)
    super(numero_de_horas, numero_de_minutos)
  puts "... en el trabajo"
end
 
  def jugar(numero_de_horas, numero_de_minutos)
    super
    puts "... en el trabajo"
  end
end

En ejecución, tenemos los siguientes resultados.

empleado = Empleado.new
empleado.dormir(2, 30) 
# => Durmiendo... por 2 horas y 30 minutos
# => ... en el trabajo
empleado.jugar(3, 10) 
# => Jugando... por 3 horas y 10 minutos
# => ... en el trabajo

Al hacer el llamado a super sin parámetros, Ruby lo detecta y le incluye automáticamente los mismos parámetros de la clase base.

Diferente número de parámetros

En un mundo ideal, se seguiría el principio de sustitución de Liskov y el caso que vamos a plantear nunca sucedería, pero en el mundo real estos errores de diseño pueden ocurrir, así que hay que contemplarlo. Si el método de la superclase recibe un número diferente de parámetros que el método de la subclase, se generará un error:

class Persona
 
  # ... todos los demás métodos
 
  def descansar(numero_de_horas, numero_de_minutos)
    puts "Descansando... por #{numero_de_horas} horas y #{numero_de_minutos} minutos"
  end
end
 
class Empleado < Persona
 
  # ... todos los demás métodos
 
  def descansar(numero_de_minutos)
    super
    puts "... en el trabajo"
  end
end

Si tratamos de ejecutar el código:

empleado = Empleado.new
empleado.descansar
# => persona.rb:5:in `descansar': wrong number of arguments (1 for 2) (ArgumentError)
# =>         from persona.rb:15:in `descansar'

Aquí se generó un problema: Ruby detectó que el llamado a super se había hecho sin parámetros y agregó automáticamente los parámetros del método de la superclase, el problema es que el número de parámetros no concuerda. Es estos casos, no nos queda más que hacer el llamado a super especificando los parámetros que utilizará.

class Persona
 
  # ... todos los demás métodos
 
  def descansar(numero_de_horas, numero_de_minutos)
    puts "Descansando... por #{numero_de_horas} horas y #{numero_de_minutos} minutos"
  end
end
 
class Empleado < Persona
 
  # ... todos los demás métodos
 
  def descansar(numero_de_minutos)
    super(0, numero_de_minutos)
    puts "... en el trabajo"
  end
end

Note cómo en la línea 15 se agrega un parámetro para que no genere el error.

# => Descansando... por 0 horas y 30 minutos
# => ... en el trabajo

Método de superclase sin parámetros y método de subclase con parámetros

Un caso más. ¿Qué sucede cuando el método en la superclase no recibe parámetros pero el de la subclase sí?

class Persona
 
  # ... todos los demás métodos
 
  def programar
    puts "Programando..."
  end
end
 
class Empleado < Persona
 
  # ... todos los demás métodos
 
  def programar(proyecto)
    super
    puts "... en el proyecto #{proyecto}"
  end
end 

Obtenemos el siguiente error:

empleado = Empleado.new
empleado.programar("Importante")
# => persona.rb:5:in `programar': wrong number of arguments (1 for 0) (ArgumentError)
# =>        from persona.rb:15:in `programar'

El error consiste en que cuando se ejecuta super, Ruby, nuevamente, le agrega los parámetros del método de la subclase, y eso provoca el fallo, ya que el método en la superclase no recibe ningún parámetro. Para estos casos, la solución es mandar a llamar al método con la lista de parámetros vacía pero utilizando los paréntesis, así se le indica a Ruby que no agregue automáticamente los parámetros de la subclase.

class Persona
 
  # ... todos los demás métodos
 
  def programar
    puts "Programando..."
  end
end
 
class Empleado < Persona
 
  # ... todos los demás métodos
 
  def programar(proyecto)
    super()
    puts "... en el proyecto #{proyecto}"
  end
end

Al ejecutarlo:

empleado = Empleado.new
empleado.programar("Importante")
# => Programando...
# => ... en el proyecto Importante

El método initialize

Si bien el principio de sustitución de Liskov se puede (no se debe, pero se puede) romper en cualquier método, es en el constructor de las clases en el que sucede con mayor frecuencia. Afortunadamente, el método initialize se comporta como cualquier otro método y por lo tanto sigue las mismas reglas que hemos expuesto aquí. Ejemplo.

class Persona
 
  def initialize
    # el código para inicializar una persona
  end
 
  # ... todos los métodos
 
end
 
class Empleado < Persona
 
  attr_reader :numero_empleado
 
  def initialize(numero_empleado)
    super()
    @numero_empleado = numero_empleado
  end
 
  # ... todos los demás métodos
 
end

Conclusiones

Al utilizar herencia en Ruby, es necesario conocer cómo interactúan los métodos dentro de la jerarquía. El uso de super es fundamental, y conociendo su funcionamiento podemos explotar mejor las capacidades del lenguaje.