martes, 12 de febrero de 2013

Tus propios objetos

Te propongo un proyecto

Ya te vas haciendo una idea de los conceptos básicos de Ruby.

Y ese es, para mí, el peor momento de todos. Porque corro el riesgo de aburrirte con explicaciones innecesarias y también de agobiarte con conceptos avanzados que quizá no estés aún en condiciones de digerir.

Así que, para ponerte un cebo que te enganche, te propongo un proyecto a desarrollar en los próximos capítulos: Un intérprete de código HTML que te permita analizar, consultar y modificar código HTML. A mí me ha sido muy útil contar con él a la hora de crear algunos scripts.

¿Qué no sabes qué es eso de HTML? No te preocupes, que, como dijo Platón, el único castigo que merece el que ignora es que le expliquen las cosas. HTML es el lenguaje en el que están escritas la mayor parte de las cosas que puedes encontrar en Internet, esas que se llaman páginas web.

No es éste el sitio ni el momento de explicar cómo es HTML “por dentro”. Pero, si no lo sabes, hay mucha documentación en Internet al respecto. Una de las que a mí me gustan es “HTML con clase”:


Ahí vas a encontrar un montón de información explicada de forma sencilla y relatada como si de un cuento se tratara. Si quieres ir al grano, lo esencial para nuestro proyecto está en:

http://html.conclase.net/tutorial/html/2/2

y, al final de

http://html.conclase.net/tutorial/html/2/4

… cuando habla de los atributos.

Si lo ves necesario, echa un vistazo por ahí. Y vuelve cuando hayas terminado. Yo seguiré aquí esperándote. Pero… ¡No me dejes tirado!


Pensando en la primera clase

Que Ruby tiene ya un montón de clases definidas, ya lo sabes. Que nunca es suficiente, es un hecho.

Para empezar, vamos a analizar un problema que nos vamos a encontrar al analizar código HTML.
HTML es muy flexible y muy permisivo. Las etiquetas y los nombres de atributos pueden escribirse en mayúsculas o minúsculas. O en combinaciones de ambas. Para un navegador, es lo mismo que pongas
<BODY>
 … que
<body>
 … que
</BoDy>

… aunque lo que se recomienda normalmente es usar letras minúsculas.

Eso nos va a causar más de un quebradero de cabeza si no lo tenemos en cuenta desde el principio.

Pensemos ahora en los atributos de un elemento. Y no olvidemos que queremos que cuando preguntemos por el valor del attributo “ID” de un elemento nos proporcione el mismo resultado que para “id”. O lo diseñamos para que funcione así ahora o más adelante, cada vez que hagamos referencia un atributo, habrá que utilizar downcase para pasarlo a minúsculas. Y como se nos pase alguna…

Así que vamos a crear una clase para representar los atributos de un elemento. La vamos a llamar Attributes (podría haber elegido Atributos, o cualquier otra cosa en español, pero tengo la costumbre de utilizar nombres ingleses). Lo primero que hay que pensar es “¿tengo ya una clase que haga algo muy parecido a lo que quiero?”.

Y sí que la tengo: Hash. Un Hash, recordarás, era parecido a un Array pero con la ventaja de que se puede utilizar un objeto como índice. De modo que podría tener un Hash como:
attributes = {'id'=>'test', 'href' => 'http://418iamateapot.blogspot.com.es/'}

… que represente los atributos de una etiqueta, junto con sus correspondientes valores, y después podría irlos recuperando mediante expresiones como
attributes["id"]

… o
attributes["href"]

Y también realizar asignaciones como
attributes["class"] = "invisible"

Pero tengo que tener en cuenta varias cosas si no quiero que el código se me complique:

    que los nombres de los atributos tienen que ser cadenas y estar siempre en minúsculas.
    que los valores de los atributos tienen que ser cadenas.

¡Ala! Valor y… ¡al toro!



Tu primera clase, a medio terminar


Vamos a crear un fichero de texto llamado “atributtes.rb” para guardar la especificación de la nueva clase que vamos a crear. Fíjate bien en el directorio en que lo guardas, que después vamos a hacer cosas en él. Y si es uno nuevo y no tocas nada que te pueda valer, mejor que mejor.

El contenido inicial de attributes.rb será:

class Attributes  < Hash
    #Creación del objeto
    def initialize(hash = {})
    end
  
    # Recuperación de un elemento del hash
    def [](index)
    end
  
    # Asignar valor a un elemento dado
    def []=(index, value)
    end
      
    # ¿Tiene el hash una clave dada?
    def has_key?(index)
    end

    # Copiar los datos a partir de un hash
    def copyHash(hash)
    end
end

 

¿Qué es todo esto?


Lo que acabamos de escribir es una definición de clase. Comienza con la palabra “class” y después va el nombre de la clase (recuerda: la primera letra del nombre de clase, en mayúsculas).

Lo de “ < Hash” indica que la clase que estamos creando es una “subclase” de Hash. O, lo que es lo mismo, que Hash es la “superclase” de Attributes.  Eso de "superclase"  no quiere decir que Hash  pueda volar ni tenga ningún sexto sentido arácnido.

Lo que quiere decir es que, a priori, Attributes “hereda” de Hash todos sus métodos. Buena cosa, porque así nos ahorramos el tener que escribirlos nosotros. Y, te lo aseguro, eso es un buen trabajo.
Por supuesto, si nuestra clase no necesitara heredar métodos de otra, no sería necesario poner el “<” y lo que sigue.

La definición de la clase termina con un “end”.  Dentro de ella aparecen las definiciones de los métodos. Como:
    def initialize(hash = {})
    end


La palabra “def” indica el inicio de la definición de un método. Le sigue el nombre del método y, si es necesario, una lista de argumentos. La lista de argumentos puede indicarse entre paréntesis o sin ellos (yo me acostumbré a usarlos y, personalmente, prefiero ponerlos).

Fíjate un poco más. El nombre del método es, en este caso, “initialize”. Este método tiene algo especial. Cuando creas un objeto nuevo, se llama automáticamente a “initialize” para inicializar su valor.

Y mira el parámetro. Su nombre es “hash”. Lo de “={}” sirve para asignar un valor por defecto. O sea, que si se invoca el método initialize sin ningún argumento es como si se le pasara {}. Esto de los valores por defecto puede ser muy útil en algunas ocasiones. Y cuando no sean precisos, no es obligatorio ponerlo.

El método termina con la palabra “end”.  O sea, que en el ejemplo anterior, los métodos están vacíos. No hacen nada todavía.


¿Qué no hacen nada? ¿Me hincho de escribir para nada?


Tranqui. Paso a paso. Vamos a irlos rellenando poco a poco. Comencemos por el tercero, “[]=”.

El método “[]=” existe en la clase Hash y se usa para asignar un valor a un elemento del hash. Es el que se usa cuando escribes
myhash["test "] = 123

Su primer parámetro es el índice y el segundo el valor. Lo que vamos a hacer es redefinirlo para que el índice siempre sea pasado a minúsculas y se asegure de que el valor se guarda como cadena. Y para ello sólo hace falta añadir una línea
    def []=(index, value)
        super(index.to_s.downcase, value.to_s)
    end

¿Super qué? ¿Qué es eso de “super(index.to_s.downcase, value.to_s)”?


Vamos a ver. Para asignar un valor a un objeto de tipo Attributes habría que:

  • Convertir el índice en una cadena en minúsculas
  • Asegurarnos de que el valor es una cadena
  • Asignar un valor a un elemento de nuestro objeto como si fuera un hash

Para convertir un objeto en una cadena está el método to_s. Y para pasarlo a minúsculas tenemos downcase. Hasta aquí, fácil.

Pero lo de asignar un valor a un elemento del hash es harina de otro costal. Porque para eso se usa el método “[]=” de la case Hash. ¡Pero ése es precisamente el método que estamos redefiniendo!

¿Quiere eso decir que ya hemos perdido "para siempre" el método "[]=" de la clase Hash? ¿Que no podremos utilizarlo?

Bueno, no del todo. Para eso es para lo que sirve la llamada a “super”:  para invocar al método de la superclase, la clase “padre”, cuyo nombre sea el mismo que el del que estamos escribiendo.

Y al “[]=” de la clase Hash, que invocamos mediante "super", le pasamos como índice el mismo que nos pasan a nosotros, pero transformado en cadena (por si acaso nos meten  otra cosa) con “to_s” y convertido a minúsculas con “downcase”. Y como valor, el mismo que nos han dado, convertido en cadena (de nuevo, por si las moscas).

Otros métodos


Espero que no hayas olvidado que el método “[]” sirve para obtener el valor de un elemento de un hash. Como en
myhash["id "]

Observa que “[]” y “[]=” son dos métodos completamente distintos. El primero permite consultar el valor de un elemento mientras que el segundo lo modifica. O, en otras palabras: El primero es sólo para lectura y el segundo es para escritura.

Vamos a redefinirlo ahora en la clase Attributes para que el valor usado como índice siempre sea una cadena en minúsculas.  Por si acaso. Es el método que pusimos en segundo lugar y así quedaría:
    def [](index)
        super(index.to_s.downcase)
    end

De nuevo, hacemos referencia al método de igual nombre de la clase padre para definir el de la nuestra. Y lo haremos otra vez en la redefinición de has_key?, un método de la clase Hash que nos indica si un hash tiene una determinada clave:
   
    def has_key?(index)
        super(index.to_s.downcase)
    end

Aparte de redefinir los métodos ya existentes, podemos añadir otros nuevos  a nuestra clase. Como copyHash, el último que pusimos en nuestro “esqueleto” y que servirá para copiar los valores almacenados en un objeto de la clase Hash a nuestro conjunto de atributos. Te propongo que lo dejemos así:

    def copyHash(hash)
        hash.each do |index, value|
            self[index] = value
        end  
    end

Y ahora te explico…



Self


Mira en la línea central de nuestro  último método y verás que pone “self[index] = value”. A estas alturas sabes suficiente de esto como para sospechar que estamos asignando un valor a un elemento de un hash (o quizá un índice) que se llama “self”.

Pero… ¿de dónde sale este “self”? Bueno, esta es de esas cosas que resultan fáciles de ver una vez que se saben:  Con self se representa el  propio objeto  al que se está aplicando el método. En este caso, el objeto de tipo Attributes al que queremos copiar los datos del hash.

Fíjate también en que estamos lllamando a uno de los métodos redefinidos, “[]=”. El método que se utilizará será el de la clase Attributes, el nuevo.


Each


Pero alrededor de esta asignación hay otra cosa rara. Antes pone
hash.each do |index, value|

... a lo que siguen una cuantas cosas. Recuerda que “hash” era el parámetro que le pasábamos al método y que se espera que sea de tipo Hash.

Empecemos por el principio.

Con “hash.each” indicamos que queremos hacer una cosa con todos los elementos contenidos en el objeto “hash” (también existe una versión de "each" para los arrays que funciona de forma similar).

Eso sí, en orden: primero con un elemento, después otro, después otro… y así hasta que no queden más. Curioso método, éste “each”.

Curioso y útil.

A continuación le decimos qué hay que hacer con cada elemento. Para eso se pone un bloque de instrucciones:
       do |index, value|
            self[index] = value
        end  

El bloque comienza con “do” y después va una lista de parámetros encerrados entre barras “|”. El efecto es que, para cada elemento del hash, vamos a tomar (iba a poner “coger”, pero creo que hay sitios donde “coger” significa otra cosa y no es de buena educación escribir “coger”, ni decir “coger”, ni "coger" en público, ni...)

 … Por donde iba... ¡Ah , sí, lo de " hash.each do |index, value| "! Eso significa que, PARA CADA ELEMENTO de hash, vamos a tomar su índice y lo vamos a llamar “index” . Y vamos a cog... tomar su valor y llamarlo “value”. Y, hecho esto, vamos a ejecutar las instrucciones que siguen, hasta llegar al "end" que cierra el bloque.

Como el "for each", o similar, de muchos lenguajes de programación. O, en terminología para gente bien: se trata de un "iterador".

En este caso, las instrucciones copian el elemento en nuestro hash de tipo Attributes.


Hay una forma alternativa de escribir los bloques en Ruby. Ocasión habrá de usarla. La sintaxis de Ruby es, como las buenas dietas, rica y muy variada.

Initialize


A estas alturas, no harán falta demasiadas explicaciones para el método “initialize”:
    def initialize(hash = {})
        super
        copyHash(hash)
    end


Primero se llama al método de inicialización de la clase padre, creando así un Hash vacío. A este objeto se le asignará la clase “Attributes”.

Después se invoca a copyHash para copiar los datos del Hash que se recibe como parámetro. Con las debidas modificaciones, claro.

Y ya tienes tu clase…


Si has ido realizando las modificaciones en la clase Attributes según ibas leyendo, tu fichero “attributes.rb” ahora contendrá:

class Attributes < Hash
    #Creación del objeto
    def initialize(hash = {})
        super
        copyHash(hash)
    end
  
    #Recuperación de un elemento del hash
    def [](index)
        super(index.to_s.downcase)
    end
  
    # Asignar valor a un elemento dado
    def []=(index, value)
        super(index.to_s.downcase, value.to_s)
    end
   
    # ¿Tiene el hash una clave dada?
    def has_key?(index)
        super(index.to_s.downcase)
    end

    #Copiar los datos a partir de un hash
    def copyHash(hash)
        hash.each do |index, value|
            self[index] = value
        end
    end end

Guárdalo y vamos a probarlo

Vamos a verle alguna utilidad al trabajo realizado


Abre un intérprete de comandos y ve al directorio en el que guardaste el fichero “attributes.rb”.

Ejecuta  ahora el intérprete de ruby, “irb”. Cuando te esté pidiendo que le des órdenes, escribe
require './attributes.rb'



Con eso hemos cargado nuestro fichero y hemos conseguido que la clase Attributes ya esté disponible y podamos utilizarla.

Creamos ahora un hash. De los normalitos:
myhash = {'abc'=>1, 'B'=>'a1', 'c'=>'b2', 'b'=>'test'}



El hash contiene cuatro elementos. Sus índices son “abc”, “B”, “c” y “b”. Con ellos se van a represntar los nombres de los atributos. Y sus correspondientes valores son:  1, “a1”, “b2” y “test”.
Fíjate en que hay un índice “B” y oro “b”. Para un hash, ambos serían distintos. Pero no para un objeto de clase Attributes. Comprobémoslo. Para empezar, creamos uno con
att = Attributes.new(myhash)



Con Attributes.new se crea un objeto de la clase Attributes. Los parámetros que se le pasan deben concordar con los que definimos en el método “initialize” de la clase, que será llamado para asignar los valores iniciales. Mira ahora el resultado:
{"abc"=>"1", "b"=>"test", "c"=>"b2"}

Y comprueba que todos los índices están ahora en minúsculas. Y que de los dos valores “B” y “b” del hash original sólo queda uno: el último. Y que, si bien el hash inicial tenía un valor numérico, 1, asociado al índice “abc”, en nuestro conjunto de atributos resultante aparece como una cadena.

Y aprovecha y sigue haciendo pruebas:

No hay comentarios:

Publicar un comentario