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, tipsEjemplos y usos de Patrones de Diseño en Ruby.
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:
-
-
recurso = ServiceFactory.instance( ‘Flickr’,'http://www.flickr.com/photos/ismasan/2224120921/’)
-
-
recurso.original #=> URL a la foto o video original
-
-
recurso.thumbnail #=> miniatura de foto o video
-
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í:
-
-
class ServiceFactory
-
# Metodo de clase para crear instancias
-
#
-
def self.instance( service_name, url )
-
subclass_name = "#{service_name}Adapter"
-
subclass = const_get(subclass_name)
-
subclass.new( url )
-
end
-
-
# Mas metodos a continuación…
-
# …
-
def original
-
# redefinido en cada subclase
-
end
-
-
def thumbnail
-
# redefinido en cada subclase
-
end
-
end
-
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).
-
-
require ‘net/http’
-
class FlickrAdapter < ServiceFactory
-
def initialize( url )
-
@url = url
-
process
-
end
-
-
# redefinimos original() y thumbnail() para rescatar los distintos tamanios de fotos de Flickr
-
#
-
def original
-
# un poco de Expresiones Regulares
-
end
-
-
def thumbnail
-
# otro poco de Expresiones Regulares
-
end
-
-
protected
-
# Aqui se hace el trabajo pesado de ir a Flickr a buscar la foto
-
#
-
def process
-
# Usar Net::Http para ir a @url a buscar la foto
-
end
-
end
-
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.
-
-
video = ServiceFactory.instance( ‘YouTube’, ‘http://www.youtube.com/watch?v=3ydGP1QQHqw’ )
-
-
video.original # => URL del .FLV original
-
-
video.thumbnail # => URL de la imagen en miniatura del video
-
-
# Adaptador para YouTube
-
#
-
class YouTubeAdapter < ServiceFactory
-
def initialize( url )
-
@url = url
-
end
-
-
# redefinimos original() y thumbnail() para rescatar los distintos tamanios de fotos de Flickr
-
#
-
def original
-
# …
-
end
-
-
def thumbnail
-
# …
-
end
-
-
protected
-
# etc etc
-
end
-
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:
-
-
recurso = ServiceParser.instance( nombre_del_servicio, url_de_la_pagina )
-
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.
-
-
SERVICIOS = {
-
FlickrAdapter => /flickr\.com/,
-
YouTubeAdapter => /youtube\.com/,
-
PhotoBucketAdapter => /photobucket\.com/
-
}
-
-
def self.instance( url )
-
subclass = SERVICIOS.find(nil) {|klass, exp| url =~ exp} # retorna un array [clase, exp]
-
raise "No existe un servicio para URL #{url}" if subclass.nil?
-
subclass = subclass.first
-
subclass.new( url )
-
end
-
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.
-
-
class ServiceFactory
-
# atributo de clase para registrar adaptadores.
-
#
-
@@adatpers = []
-
-
# Accessor para array @@adapters
-
#
-
def self.adapters
-
@@adapters
-
end
-
-
# …
-
end
-
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.
-
-
class FlickrParser < ServiceFactory
-
ServiceFactory.adapters << [self, /flickr\.com/]
-
-
#…
-
end
-
-
class YouTubeParser < ServiceFactory
-
ServiceFactory.adapters << [self, /youtube\.com/]
-
-
#…
-
end
-
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.
-
-
ServiceFactory.adapters < < [self, /youtube\.com/]
-
Cuando todas las subclases se han registrado con la Superclase, necesitamos modificar ServiceFactory.instance() para usar este nuevo enfoque.
-
-
def self.instance( url )
-
subclass = @@adapters.find(nil) {|klass, exp| url =~ exp} # retorna un array [clase, exp]
-
raise "No existe un servicio para URL #{url}" if subclass.nil?
-
subclass = subclass.first
-
subclass.new( url )
-
end
-
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:
-
-
class FlickrAdapter < ServiceFactory
-
# DSL para registrarse con la Superclase
-
#
-
register_url /flickr\.com/
-
end
-
Implementamos el método de clase register_url en la Superclase.
-
-
class ServiceFactory
-
# usamos este metodo de clase en las subclases
-
#
-
# Opciones:
-
# +exp+ una expresion regular
-
#
-
# cuando es llamado desde una subclase, "self" equivale a la subclase
-
#
-
def self.register_url( exp )
-
ServiceFactory.adapters < < [self, exp]
-
end
-
-
# …
-
end
-
- Servicios:
- Comentarios RSS
- Menear!
- Del.icio.us

2/1/2008 at 11:31 am
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/1/2008 at 12:44 pm
Buen trabajo, yo personalmente aplicaba estos patrones dentro del lenguaje c#, pero veo que en ruby es bastante facil y compresible.
Saludos
2/1/2008 at 1:29 pm
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.
2/25/2008 at 9:48 am
[…] mi artículo anterior explicaba cómo podemos usar la maleabilidad de Ruby para solucionar problemas espinosos. […]
3/18/2008 at 9:28 pm
[…] 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 […]
5/30/2008 at 11:03 am
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