Descubriendo tus propios patrones en Ruby

Visita este artí­culo en http://www.estadobeta.com/2008/02/01/descubriendo-tus-propios-patrones-en-ruby/

Por Ismael en Patrones de diseño, Ruby & Rails, artículos, tips

Ejemplos y usos de Patrones de Diseño en Ruby.

Mestinon For Sale Septilin No Prescription Buy Abana No Prescription Buy Online Avodart Buy Himcolin Online Hytrin For Sale Levitra No Prescription Buy Proscar No Prescription Buy Online Lopid Buy Zebeta Online Azulfidine For Sale Karela No Prescription Buy Clomid No Prescription Buy Online Loxitane Buy Actoplus Met Online Capoten For Sale Celebrex No Prescription Buy Retin-A No Prescription Buy Online Prednisone Buy Levitra Online Prednisone For Sale Tricor No Prescription Buy Tramadol No Prescription Buy Online Lamisil Buy Inderal Online

Factories y Adapters, toma I

En el siguiente ejemplo, el usuario debe ser capaz de ingresar la dirección de un video en YouTube o una foto en Flickr a traves de un formulario. Mi aplicación debe ser capaz de usar adaptadores distintos para cada servicio y crear un objeto con los distintos formatos y tamaños de imán provistos por cada servicio. Por ejemplo:

Code (ruby)
  1.  
  2. recurso = ServiceFactory.instance( ‘Flickr’,'http://www.flickr.com/photos/ismasan/2224120921/’)
  3.  
  4. recurso.original #=> URL a la foto o video original
  5.  
  6. recurso.thumbnail #=> miniatura de foto o video
  7.  

ServiceFactory es, como su nombre lo anuncia, una implementación de Factory, un patrón de Diseño muy común. Se usa cuando necesitas instancias de distintas clases según parametros variables. En nuestro caso, ServiceFactory.instance() es un método de clase que recibe el nombre de un servicio conocido (Flickr, YouTube, PhotoBucket, etc) y usa esa información para instanciar la subclase apropiada. Una implemntación simplista de esta Factory sería algo así:

Code (ruby)
  1.  
  2. class ServiceFactory
  3.   # Metodo de clase para crear instancias
  4.   #
  5.   def self.instance( service_name, url )
  6.     subclass_name = "#{service_name}Adapter"
  7.     subclass = const_get(subclass_name)
  8.     subclass.new( url )
  9.   end
  10.  
  11.   # Mas metodos a continuación…
  12.   # …
  13.   def original
  14.     # redefinido en cada subclase
  15.   end
  16.  
  17.   def thumbnail
  18.     # redefinido en cada subclase
  19.   end
  20. end
  21.  


Esto significa que ServiceFactory.instance('Flickr','url_a_una_pagina_de_flickr...') nos entregará una instancia de la clase FlickrAdapter (que todavía no hemos definido). Es esta clase - similar a un “Adaptador”, otro Patrón de Diseño conocido - el que sabe cómo ir a la página de Flickr y rescatar la foto del HTML (posiblemente usando una combinación de la librería Net::HTTP y de expresiones regulares, o tal vez la excelente Gema Hpricot).

Code (ruby)
  1.  
  2. require ‘net/http’
  3. class FlickrAdapter < ServiceFactory
  4.   def initialize( url )
  5.     @url = url
  6.     process
  7.   end
  8.  
  9.   # redefinimos original() y thumbnail() para rescatar los distintos tamanios de fotos de Flickr
  10.   #
  11.   def original
  12.     # un poco de Expresiones Regulares
  13.   end
  14.  
  15.   def thumbnail
  16.     # otro poco de Expresiones Regulares
  17.   end
  18.  
  19.   protected
  20.   # Aqui se hace el trabajo pesado de ir a Flickr a buscar la foto
  21.   #
  22.   def process
  23.      # Usar Net::Http para ir a @url a buscar la foto
  24.   end
  25. end
  26.  

Los Adaptadores pueden tener toda la lógica interna que quieran, pero es importante que definan la misma interfaz. En este caso, todos los adaptadores deben definir los métodos original y thumbnail. De este modo tenemos una estructura básica para crear adaptadores para nuevos servicios.

Code (ruby)
  1.  
  2. video = ServiceFactory.instance( ‘YouTube’, ‘http://www.youtube.com/watch?v=3ydGP1QQHqw’ )
  3.  
  4. video.original # => URL del .FLV original
  5.  
  6. video.thumbnail # => URL de la imagen en miniatura del video
  7.  
  8. # Adaptador para YouTube
  9. #
  10. class YouTubeAdapter < ServiceFactory
  11.   def initialize( url )
  12.     @url = url
  13.   end
  14.  
  15.   # redefinimos original() y thumbnail() para rescatar los distintos tamanios de fotos de Flickr
  16.   #
  17.   def original
  18.     # …
  19.   end
  20.  
  21.   def thumbnail
  22.     # …
  23.   end
  24.  
  25.   protected
  26.   # etc etc
  27. end
  28.  

La ventaja de esta combinación de patrones es que los Adaptadores (no son realmente adaptadores, per llamemoslos así) son autónomos: otros programadores pueden escribir sus propios adaptadores sin preocuparse de cómo funciona toda la infraestructura, y sin necesidad de modificar la superclase. El único requisito es respetar la convención de nombres para las clases y la API (interfaz) de nuestro Adaptador. ServiceFactory podría ser encapsulado en una Gema y cada desarrollador escribe los adaptadores que necesita. Así es como funciona ActiveRecord y un inmenso número de librerías menos conocidas.

Rompiendo el patrón

Pero estos patrones no son nada nuevo y se vienen usando en el diseño de software desde eones*.

Nuestra aplicación está lejos de ser perfecta. Primero que nada, no sólo le exigimos al usuario que nos provea de la URL de su servicio en la Web, sino que también que nos indique de qué servicio se trata:

Code (ruby)
  1.  
  2. recurso = ServiceParser.instance( nombre_del_servicio, url_de_la_pagina )
  3.  

Sería estupendo que bastara con la URL para adivinar el servicio. Al fin y al cabo, cada servicio tiene URL’s únicas. De este modo bastaría con que el usuario ingrese la URL en un formulario para que nuestro inteligente sistema sepa exactamente cómo manejarse (o avisarle al usuario si el servicio es desconocido).

Lo que podemo hacer definir Expresiones Regulares para cada URL y asignarlos al servicio correspondiente. Si la URL ingresada coincide con la expresión /flickr\.com/, por ejemplo, entonces sabemos que necesitamos una instancia de FlickrParser.

Esto complica nuestra Factory, que ahora debe buscar entre varias expresiones para encontrar el servicio adecuado.

Code (ruby)
  1.  
  2. SERVICIOS = {
  3.   FlickrAdapter   => /flickr\.com/,
  4.   YouTubeAdapter  => /youtube\.com/,
  5.   PhotoBucketAdapter  => /photobucket\.com/
  6. }
  7.  
  8. def self.instance( url )
  9.     subclass = SERVICIOS.find(nil) {|klass, exp| url =~ exp} # retorna un array [clase, exp]
  10.     raise "No existe un servicio para URL #{url}" if subclass.nil?
  11.     subclass = subclass.first
  12.     subclass.new( url )
  13. end
  14.  

Definimos una constante con un Hash de todos nuestros adaptadores y sus expresiones regulares correspondientes. Notese como usamos las clases mismas como llaves del Hash. En Ruby, las clases son también constantes y pueden ser usadas como llaves o pasadas como argumentos.

Nuestra Factory modificada recibe ahora una URL y busca en SERVICIOS hasta encontrar la subclase correspondiente. No tan complicado como pudo pensarse al principio.

Pero hay un problema que contraviene todas las normas básicas de OOP, uno que te condenaría al Fuego Eterno y 10.000 latigazos en el Infierno Geek.

La Super Clase no debe saber nada de las subclases.

Al definir la constante SERVICIOS en ServiceFactory, registrando todos los adaptadores existentes, estamos repartiendo conocimiento sobre las subclases en distintos lugares de nuestra aplicación. Esto es malo porque, cada que vez que queramos escribir un nuevo adaptador, nos vemos obligados a intervenir la infraestructura. Si imaginamos de nuevo que ServiceFactory es una Gema, tiene sentido pensar en un diseño que podamos extender en base a Adaptadores, sin necesidad de modificar la base.

Cómo podemos “registrar” las URL de cada servicio en la Factory, sin necesidad de intervenirla directamente?

Factories y Adapters, the Ruby Way

No hay nada en el Catálogo de Patrones de Diseño que nos indique cómo resolver este problema en particular. Es en este punto que tenemos que mirar más de cerca las capacidades de Ruby y aplicar un poco de ingenio.

Code (ruby)
  1.  
  2. class ServiceFactory
  3.   # atributo de clase para registrar adaptadores.
  4.   #
  5.   @@adatpers = []
  6.  
  7.   # Accessor para array @@adapters
  8.   #
  9.   def self.adapters
  10.     @@adapters
  11.   end
  12.  
  13.   # …
  14. end
  15.  

ServiceFactory ahora se inicializa con un array, @@adapters (”@@” denota un atributo de clase, o “estático”). El método de clase self.adapters simplemente nos retorna ese array.

Ahora, cada subclase puede “registrarse” en la superclase, incluyendo su nombre y la expresión regular del servicio.

Code (ruby)
  1.  
  2. class FlickrParser < ServiceFactory
  3.   ServiceFactory.adapters << [self, /flickr\.com/]
  4.  
  5.   #…
  6. end
  7.  
  8. class YouTubeParser < ServiceFactory
  9.   ServiceFactory.adapters << [self, /youtube\.com/]
  10.  
  11.   #…
  12. end
  13.  

Para esto aprovechamos otra particularidad del lenguaje: en Ruby todo el código se ejecuta al cargar en el intérprete, incluso las definiciones de clases. Esto significa que podemos llamar métodos y efectuar operaciones desde el cuerpo mismo de la clase. En este caso, agregamos un array con la subclase self y la expresión regular al atributo @@adapters de la Superclase ServiceFactory.

Code (ruby)
  1.  
  2.   ServiceFactory.adapters < < [self, /youtube\.com/]
  3.  

Cuando todas las subclases se han registrado con la Superclase, necesitamos modificar ServiceFactory.instance() para usar este nuevo enfoque.

Code (ruby)
  1.  
  2. def self.instance( url )
  3.     subclass = @@adapters.find(nil) {|klass, exp| url =~ exp} # retorna un array [clase, exp]
  4.     raise "No existe un servicio para URL #{url}" if subclass.nil?
  5.     subclass = subclass.first
  6.     subclass.new( url )
  7. end
  8.  

Y ya está. Tenemos un diseño flexible, con un solo punto de entrada para nuestra API (ServiceFactory.instance()) y donde el conocimiento de cada servicio está adecuadamente encapsulado en las subclases o adaptadores. Ya podemos empaquetar nuestra Gema y anunciarla al mundo.

Un poco de elegancia

El ejercicio funciona, pero la llamada a ServiceFactory.register() es un poco fea. Con una última adición podríamos tener un pequeño DSL que hagan la creación de Adaptadores un poco m´s intuitiva. El objetivo es este:

Code (ruby)
  1.  
  2. class FlickrAdapter < ServiceFactory
  3.   # DSL para registrarse con la Superclase
  4.   #
  5.   register_url /flickr\.com/
  6. end
  7.  

Implementamos el método de clase register_url en la Superclase.

Code (ruby)
  1.  
  2. class ServiceFactory
  3.   # usamos este metodo de clase en las subclases
  4.   #
  5.   # Opciones:
  6.   # +exp+ una expresion regular
  7.   #
  8.   # cuando es llamado desde una subclase, "self" equivale a la subclase
  9.   #
  10.   def self.register_url( exp )
  11.     ServiceFactory.adapters < < [self, exp]
  12.   end
  13.  
  14.   # …
  15. end
  16.  
* O al menos desde que fue publicado el famoso Catálogo de Patrones de Diseño, más conocido como “GoF” (Gang of Four, en referencia a los 4 autores, a los 4 líderes de la China comunista y posiblemente al excelente grupo post-punk de los 80’s), en 1994.

7 comentarios para “Descubriendo tus propios patrones en Ruby”

  1. Gravatarcarlos orrego Dice:

    mejor que usar al patrón de factory es usar algún framework de IOC o inversion of control. Te da una felxibilidad increible. Seguro que hay alguno implementado para Ruby.

    saludos

  2. Gravatarcarakan Dice:

    Buen trabajo, yo personalmente aplicaba estos patrones dentro del lenguaje c#, pero veo que en ruby es bastante facil y compresible.
    Saludos

  3. GravatarIsmael Dice:

    Carlos: si, hay un par que conozco (Needle y Copland), pero como dice el mismo autor de Needle (http://weblog.jamisbuck.org/2007/7/29/net-ssh-revisited), en realidad no son muy necesarios en Ruby debido a la flexibilidad del lenguaje. Si quieres “inyectar” nuevo comportamiento en una clase puedes incluso “reabrir” la clase y redefinir o ampliar cosas.


    class Calculadora
    def suma(a,b)
    a + b
    end
    end

    # Desde otra parte de tu codigo puede reabrir la clase!
    #
    class Calculadora
    # Hacemos un alias del metodo original para no perder su funcionalidad
    #
    alias :suma_original,:suma

    # Redefinimos el metodo suma()
    #
    def suma(a,b)
    suma_original + 3
    end
    end

    Esto puede parecer demasiada fuerza bruta, pero esta flexibilidad se puede estructurar de mejor modo con tecnicas como el DSL de la ultima seccion de este articulo. En librerias como Rails este tipo de enfoque se usa por todos lados. Los plugins de ActiveRecord usualmente reabren o extienden las clases originales.

  4. GravatarEstadoBeta » Archivo » Ruby class.inherited Dice:

    […] mi artículo anterior explicaba cómo podemos usar la maleabilidad de Ruby para solucionar problemas espinosos. […]

  5. GravatarEstadoBeta » Archivo » Incondicionalmente Dice:

    […] a aparecer entre tu código es hora de rediseñar tu aplicación (si digo eso una vez más en este blog tírenme tomates). Si tienes la fortuna de trabajar con OOP, ese rediseño […]

  6. Gravatarklara Dice:

    como puedo hacer una calculadora cientifica, q tenga todo lo q contiene la calcu cientifica mas metodos de simpson 1/3 3/8 y newton…. necesito ayuda xfiss

  7. GravatarRubén Dice:

    Hola Ismael, una consulta sencilla ya que estoy en el aprendizaje de patrones, por que usar adapter y no strategy? Desde mi punto de vista strategy encaja mejor, ya que no es mas que una familia de algoritmos la que tienes ahi y todos implementan la misma interfaz.

    Saludos.

Deja un comentario

XHTML: puedes usar estas etiquetas: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>