Páginas sueltas de Ruby: El modelo de objetos. Parte 1

En este blog me propongo introducir un poco el modelo de objetos de Ruby en una primera parte. Básicamente el plan es tener 3 post con el siguiente contenido:

  • Parte 1: El modelo básico de Ruby: módulos y clases.
  • Parte 2: El modelo con clases «propias» (llamadas eigenclasses)
  • Parte 3: Patrones de meta-programación que utilizan el modelo de objetos. Usos y abusos.

Este post sería el primero de este plan. Al final, la idea de estos post es motivarlos a la lectura de un libro tan bueno como Metaprogramming in Ruby.


Comencemos con las bases.

Objetos en Ruby. Mensajes

En Ruby, todo (absolutamente todo) es un objeto. Hay varios lenguajes donde esto es una realidad ya. Hay, sin embargo, una diferencia filosófica (que proviene según tengo entendido de Smalltalk) entre el Ruby y otros lenguajes OO como el Python [1]:

Lo único que se puede hacer con un objeto es enviarle mensajes.
Al recibir un mensaje el objeto intenta localizar un método con el mismo nombre y ejecutarlo.

Esto significa que en Ruby siempre que veamos algo como objeto.mensaje lo que viene después del punto es siempre un método; no un atributo, no una propiedad. Como veremos más adelante, lo que puede variar es la forma que en programamos este método.

Para ver la diferencia con Python, note que en Python objeto.attr siempre se refiere a un atributo del objeto; y si ponemos objeto.attr() ese atributo debe implementar el protocolo callable para poder ser invocado (además de que debe recibir, al menos, un argumento: el convencional self).

En Ruby, por tanto, al invocar:

1.class

Realmente se está ejecutando un método, no se está accediendo a una propiedad ni atributo del objeto 1. De hecho, todas las “funciones” que se pueden invocar sin poner el objeto son, por lo general, métodos del Kernel. Las siguientes sentencias son equivalentes e invocan el mismo método:

puts "Hola mundo!"
Kernel.puts "Hola mundo!"

Por cierto, demostrar que puts y Kernel.puts son el mismo método es una de las cosas que es más fácil en Python que en Ruby.

El estado de los objetos

Pues bien, dado que en Ruby todo son mensajes (i.e. métodos), en mi opinión, esto simplifica la forma en que podemos interactuar con los objetos. ¿Cómo entonces se almacena el estado de los objetos en Ruby?

Al implementar una clase (o módulo) en Ruby los objetos pueden tener atributos que solo son accesibles desde la implementación de los métodos. (Debo advertir que estoy simplificando un poco la explicación, solo para dar los detalles que en esta introducción son más significativos).

Una puerta en Ruby:

class Door
  def open?
    @open
  end

  def open
    @open = true
  end

  def close
    @open = false
  end
end

En Ruby, dentro de un método, las variables de instancia, que son aquellas que mantienen el estado del objeto, comienzan con el símbolo @. El resto son variables locales al método. En el ejemplo anterior, la variable @open nos ayuda a mantener el estado de la puerta. No hay forma [2] de acceder a esta variable desde fuera del objeto. Por tanto los métodos son la forma de cambiar y obtener el estado del objeto:

door = Door.new
door.close
door.open? # => false
door.open
door.open? # => true

Teniendo este bosquejo fugaz de cómo se presentan los objetos en Ruby, pasemos al plato fuerte de este post.

Clases y módulos en Ruby

En el primer ejemplo de este post, invocábamos al método class del objeto 1. Si ejecutan este método usando el intérprete interactivo de Ruby (irb) verán que retorna la clase Fixnum. Ahora ejecuten:

1.class.class # Class

Bueno, puesto indica que la clase del objeto Fixnum es Class. No deje de notar que Fixnum es una clase. Veamos el árbol de herencia:

1.class.superclass                            # => Integer
1.class.superclass.superclass                 # => Numeric
1.class.superclass.superclass.superclass      # => Object

Evidentemente la clase de todas estas clases es Class (De hecho, la clase de Class es Class). Lo curioso es que:

Class.superclass.superclass # => Object

Es decir que Class hereda de Object aunque indirectamente, puesto que:

Class.superclass # => Module

Y aquí entran los módulos. La clase Class en Ruby hereda de la clase Module. En Ruby el módulo es la unidad de abstracción de comportamiento básica. Las clases añaden (por eso Class hereda de Module) a esta unidad la posibilidad de heredar unas de otra (herencia simple) y la posibilidad de crear instancias. Con un módulo de Ruby se puede programar todo lo que hacemos normalmente con una clase, excepto que no podemos heredar de ellos ni ellos pueden heredar, no tiene herencia, y no podemos crear instancias de un módulo.

¿Entonces para qué sirven?

Hay dos usos fundamentales para los módulos. El más simple es para crear espacios de nombres:

module GESPRO
  class SomeClass
    # ....
  end
end

No exploraremos mucho este uso de los módulos puesto que es simple de entender y no aporta mucho al modelo de objetos.

El segundo es precisamente para definir (o encapsular si lo prefieren) comportamiento. ¿Por qué usar módulos y no clases para esto? Pues a veces el comportamiento deseado es ortogonal a la jerarquía de clases que tenemos a mano.

Pongamos un ejercicio mental para ver este punto. Suponga que tiene un laberinto donde hay puerta, puentes levadizos, túneles, escaleras, agujeros de gusano. También hay palancas que abren o cierran puertas, o bien suben o bajan puentes; activan o desactivan un agujero de gusano, enciende o apagan luces, ventiladores, la TV. También hay controles remotos que funcionan de forma similar a las palancas pero solamente si el objetivo está en la misma habitación. Tenemos fósforos que podemos encender y que se consumen y no se puede reutilizar. Tenemos interruptores que funcionan muy parecido a las palancas. Tenemos cerraduras que requieren de una palabra clave para abrir la puerta…

Es quizá difícil encontrar una forma de unificar todos estos objetos con sus comportamientos en una única jerarquía de objetos (en un único laberinto) sin violar el principio DRY [3].

Básicamente podemos decantar dos propiedades en los objetos mencionados:

  • Algunos exhiben la característica de que (en cierto sentido) está abiertos o cerrados. Las puertas por ejemplo, se abren o cierran, las palancas e interruptores están en On u Off; los puentes están levantados o no; las luces, ventiladores, las velas, los fósforos están encedidos o apagados.

    De hecho, algunos de estos objetos como los interruptores y palancas pueden afectar el estado de otros: las palancas e interruptores pudieran abrir o cerrar puertas, encender o apagar luces, etc…

  • Varios de estos objetos sirven de enlace entre las habitaciones como las puertas, túneles, puentes levadizos, agujeros de gusanos, etc. Excepto los túneles y los puentes fijos, todos los demás, presentan la primera propiedad también.

De hecho, algunos objetos como las velas y fósforos, pueden consumirse completamente y apagarse; y luego no se pueden encender nuevamente.

Estas unidades de comportamiento (casi puro) pueden ser representadas por módulos en Ruby. Por ejemplo:

module Switchable
  def initialize(*args)
    super(*args)
    @switchable_status ||= false
  end

  def on
    @switchable_status = true
  end

  def off
    @switchable_status = false
  end

  def switch
    if is_on?
      off
    else
      on
    end
  end

  def is_on?
    @switchable_status == true
  end
end

Este módulo representa el núcleo básico de cualquier objeto que puede estar en uno de dos estados (puertas, puentes levadizos, interruptores). Si bien los nombres de los métodos no son los más saludables para todos los objetos, eso lo podremos corregir después.

De hecho, note que el método switch no cambia directamente el valor de @switchable_status, sino que utiliza los otros métodos. ¿Por qué? Hay dos razones.

La primera: Ante la pregunta de qué hace (por ejemplo) un interruptor, la mayoría de las personas dirían “apaga o enciende la luz”; algunas agregarían: “si está encendida la apaga, y si está apagada la enciende”; pues el método se acerca a esto: “si es_on? hacer off, sino hacer on”.

La segunda es porque más adelante (quizá en otro post) incorporaremos más comportamiento a los métodos off y on. Si el método switch cambiara directamente el valor de @switchable_status se pudiera romper la lógica del objeto. Para poner un ejemplo, cuando vayamos a incluir una cerradura que requiera un password (o una puerta que para abrirse necesite que le digan “Ábrete sésamo”) el método on no sería tan simple, y aún así en Ruby podremos el método switch pudiera funcionar [4].

Para que este módulo tenga cierto uso alguna clase lo debe incluir:

class Door
  include Switchable
end

Si nuestras puertas no tienen más elementos, esta sería prácticamente toda la implementación de una puerta. Si la clase puerta hay que crearla en tiempo de corrida:

klass = Class.new
klass.send :include, Switchable
Door = klass   # Esta línea no es muy dinámica pero le pone el
               # nombre a la clase

Luego podemos crear puertas -y ventanas ;):

door = Door.new
door.on
door.off

Las últimas líneas no me gustan mucho porque las puertas no se encienden ni se apagan. Por tanto, ahora puedo abrir la clase Door y crear dos alias:

class Door
  alias_method :open, :on
  alias_method :close, :off
end

Ahora todas las puertas (incluso las instancias que ya están creadas) tienen los métodos open y close:

door.open
door.close

Epílogo

En este post he brindado un esbozo del modelo de objetos de Ruby. Esta es una introducción básica que obvia varios detalles.

Si usted encuentra algún error en los códigos puesto en el post, o en los pastes relacionados, le agradeceré que ponga un comentario para corregirlo.

Notas al pie

[1] Utilizo Python como lenguaje de comparación en este post porque es otro de mis lenguajes favoritos. Adicionalmente es más fácil comparar el Ruby con Python debido a que ambos son lenguajes dinámicos con duck-typing. Debo aclarar además, que no es la intención de este post decir que Ruby es mejor o peor que Python. Solamente utilizo el Python como un punto de comparación para entender mejor el Ruby.

[2] Otra simplificación mía, puesto que sí existe una forma de meterse con estado interno de un objeto:

door = Door.new
door.instance_eval { @open = :hacked }
door.open?  # => :hacked

Pero esto puede romper completamente las expectativas del programa.

[3] En este post no indagaré ninguna solución que implique la multiherencia. Prefiero pensar en clases en que la herencia juegue el papel de una clasificación centrada en la propiedad de que “todo A es un también un B” lo que significaría que A hereda de B; y otras restricciones sobre la naturaleza de A y B.
[4] El módulo Switchable que mostramos en este post “introductorio” está incompleto. Una versión completa puede verse en el paste https://gist.github.com/1260817#file_switchable.rb No obstante, incluso en este ejemplo me limitado un poco; puesto que una versión más apropiada de este ejercicio utilizaría muchos más elementos como instance_eval y method_missing para poder combinar los comportamientos programados en los módulos mucho más libremente.

About these ads
This entry was posted in programming and tagged , . Bookmark the permalink.

One Response to Páginas sueltas de Ruby: El modelo de objetos. Parte 1

  1. Pingback: Páginas sueltas de Ruby: El modelo de objetos. Parte 2 « Manuel on Software

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s