Necesitas algo más flexible que Module#delegate de Rails, entonces Module#delegate_method
Publicado por el Viernes, 26 de Octubre de 2007
Rails a través de una extensión de la clase Module en ActiveSupport permite delegar métodos de manera comoda. Veamos un ejemplo, para entenderlo:
class A
def a
'a'
end
def aa
'aa'
end
end
class B
attr_accessor :the_a
def initialize(the_a)
@the_a = the_a
end
delegate :a, :to => :the_a
end
the_b = B.new(A.new)
the_b.a # => "a"
Interesante, pero con limitaciones. Probemos otras posibiliades:
class B delegate :a2, :to => :the_a end the_b = B.new(A.new) the_b.a2 # => NoMethodError: undefined method `a2' ... the_b = B.new(nil) the_b.a # => NoMethodError: You have a nil object... the_b.a2 # => NoMethodError: You have a nil object...El código causante de este comportamiento tiene la siguiente pinta:
def delegate(*methods)
options = methods.pop
unless options.is_a?(Hash) && to = options[:to]
raise ArgumentError, "Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, :to => :greeter)."
end
methods.each do |method|
module_eval("def \#{method}(*args, &block)\n\#{to}.__send__(\#{method.inspect}, *args, &block)\nend\n", "(__DELEGATION__)", 1)
end
end
Esto no nos agrada del todo, ya que lo interesante sería que se pudiesen escribir cosas como las siguientes sin preocuparse:
p1.product.name # => 'FypURL' p1.product.brand.name # => "Trabe" p1.product.brand_name # => "Trabe" p2.product.name # => "Unbranded product" p2.product.brand.name # => NoMethodError: You have a nil object... p2.product.brand_name # => "unknown"
Aqui os pongo nuestro Module#delegate_method que permite todo esto:
class Module
def delegate_method(method, options)
to,to_method = options[:to].to_s.split('.')
to_method = method if to_method.nil?
default = options[:default]
code = %{
def #{method}(*args, &block)
if #{to}
#{to}.__send__(#{to_method.inspect}, *args, &block) || #{default.inspect}
else
#{default.inspect}
end
end
}
module_eval code, __FILE__, __LINE__
end
end
Esta versión se diferencia de Module#delegate_method anterior en lo siguiente:
No reescribimos el método original ya que la implementación no es compatible al 100% con la anterior. No soportamos la definición de multiples delegaciones y no queremeos que código ya escrito deje de funcionar. Si queremos delegar del modo Rails seguimos teniendo
Module#delegate. La posibilidad de definir varias delegaciones en una sola sentencia no es una característica fundamental (para delegar muchos métodos a lo mejor compensa trastear con elmethod_missing).- No chequeamos la existencia del parametro :to (presuponemos que vamos a escirbir correctamente el código y si no es asís ya saltará otra expceción.
Permitimos delegar en un método diferente al método delegado. Esto es super útil como se vió en el ejemplo de productos y marcas.
Si no existe el delegado o éste no tiene valor obtenemos
nily no una sucia excpeción, o lo que es mejor, obtenemos un valor por defecto.
Probemos de nuevo el código con el que abrimos el post sutituyendo delegate por delegate_method
class B ... delegate_method :a, :to => :the_a, :default => 'default' delegate_method :aa, :to => 'the_a.aa' end the_b = B.new(A.new) the_b.a # => "a" the_b.aa # => "aa" the_b = B.new(nil) the_b.a # => default the_b.aa # => nil
Extensiones al core de Ruby, copia profunda de objetos con el api de marshalling (serializando, vamos)
Publicado por el Martes, 23 de Octubre de 2007
Una necesidad típica a la hora de programar es copiar o clonar objetos. Los métodos de Ruby Object#clone y Object#dup realizan copias superficiales (shallow), es decir, si el objeto contiene referencias a otros objetos se duplican las referencias pero no los objetos apuntados. Si la copia fuese profunda (deep), también se duplicarían los objetos referenciados. Ejemplo:
h = { 1 => ['a','b'], 2 => ['c'] }
h1 = h.clone
h2 = h.deep_clone
h1[1].shift # => "a"
h # => {1=>["b"], 2=>["c"]}
h1 # => {1=>["b"], 2=>["c"]}
h2 # => {1=>["a", "b"], 2=>["c"]}
Lo primero que se nos puede ocurrir es crear un método deep_clone específico para cada objeto (o incluso uno más genérico que, mediante introspección del objeto supiese que copiar). Ésto es tedioso y tiene sus problemas (básicamente el meanejo de referencias cruzadas), además, eso no es DRY. Por fortuna Ruby tiene soporte para serialización (módulo Marshal). El truco es sencillo: serializo, deserializo.
class Object
def deep_clone
# Abracadabra !!!
Marshal::load(Marshal::dump(self))
end
end
Sencillo, bello, y todas esas cosas que tanto gustan de Ruby.
A esta técnica sólo podemos ponerle dos pegas: 1) si una parte de la clase no es serializable no podremos copiarla, y 2) es cierto que una implementación ad hoc de copia puede ser más rápida (pero yo, al menos, estoy dispuesto a perder esa velocidad).
PD: Por cierto, esta técnica tan sencilla es trasladable directamente a cualquier otro lenguaje/plataforma con soporte para serialización: Java, .NET, etc.
Ponencia de Trabe Soluciones aceptada en la Conferencia Rails 2007
Publicado por el Martes, 23 de Octubre de 2007
Ayer, la organización de la Conferencia Rails 2007 me confirmó la inclusión de una de nuestras ponencias en el programa de las conferencias. Podéis ver la relación completa en la dirección http://ponencias.conferenciarails.org.
De las dos propuestas que enviamos aceptaron una que titulamos Proyectos de bajo coste con limitaciones severas de tiempo y recursos y dejaron fuera otra de APIs Rest y proxificación, lo cual me ha sorprendido bastante. Sin quitarle interés a la primera ponencia, creo que la segunda era mucho más interesante y amena (mucho más tecnológica y menos empresarial). En cualquier caso nos vemos en la Conferencia Rails en Noviembre.
PD: Aunque el que escribe aparece como ponente en la página web, debo decir que este punto no está confirmado y puede dar la charla cualquier otro miembro de Trabe. Más adelante os confirmo a quién le podéis tirar los tomates el día de la charla.
Symbol#to_proc de rails y otras alternativas (o locuras, según se mire)
Publicado por el Jueves, 18 de Octubre de 2007
Este post es consecuencia directa de un comentario de Sergio Gil a mi post anterior: Extensiones al core de Ruby, pequeños pedazos de código que harán tu vida más sencilla. Hoy Enumerable.pluck y Enumerable.invoke.
Sergio, acertadamente comenta:
ActiveSupport añade a each y a collect/map una versión abreviada de la sintaxis (realmente lo que hace es definir Symbol#to_proc, si no me equivoco) que la deja muy parecida a lo que habéis creado, que dejaría el ejemplo así:
objects.collect(&:key)
a lo que nosotros respondemos:
Tienes razón Sergio, existe esa posibilidad, aunque aquí en Trabe consideramos que es una construcción un poco oscura. Por otro lado la versión que utilizamos nosotros, permite hacer alguna cosa adicional fácilmente, por ejemplo, utilizar una cadena como parámetro en lugar de un símbolo (que puede considerarse algo menor) o más interesante, pasar parámetros a la función que se invoca (que como pone el post, queda como ejercicio para el lector ;)
Ejemplo práctico:
%w(un ejemplo).pluck(’*’,2) => [“unun”, “ejemploejemplo”]
module Enumerable
def invoke(method_name, *args)
self.each { |i| i.send(method_name, *args) }
end
def pluck(method_name, *args)
self.collect { |i| i.send(method_name, *args) }
end
end
Hasta aquí todo bien. Lo que pasa, es que cuando se escribe en un blog y se reciben comentarios uno se ve obligado a realizar un ejercicio mental y reflexionar mucho sobre lo que escribe. Así que me he tomado un ratito para investigar algo más sobre este tema.
La primera carencia que apuntamos acerca de la solución con Symbol#to_proc es fácilmente superable, sólo necesitamos definir nuestro propio String#to_proc. La falta de parámetros en el método es algo que me llama especialmente la atención. Estuve haciendo unas pruebas en la consola, y no conseguí pasar nigún parametro, así que me fui al código fuente y esto es lo que pone:
def to_proc
Proc.new { |*args| args.shift.__send__(self, *args) }
end
Dado que no soy un gurú ni un semidios de Ruby no puedo afirmar esto con rotundidad, pero, a primera vista parece que to_proc devolvería un Proc capaz de recibir un número variable de argumentos.
Ante la duda, Google, porque no estamos sólos en elmundo.
Lo que descubrí, a parte de confirmar que Symbol#to_proc no puede recibir argumentos, es la Methodphitamine (algo así como la método-fetamina), una suerte de locura sintáctica que permite hacer algo como:
(1..100).select &it % 2 == 0
File.read("/etc/passwd").split.sort_by &it.split(":")[2]
User.find(:all).map &its.contacts.map(&its.last_name.capitalize)
¡Qué locura!, ¿Efectos de la droga?, ¿Tendrá este hombre que mear en un vaso y pasar un anti-dopping? en fin. Yo, personalmente, comulgo con un comentario de Sam Aaron aparecido en The Methodphitamine: A Better Symbol#to_proc?. I’m not a big fan of the to_proc hack in the first place. To my eyes, it’s ugly, and Methodphitamine is even more ugly. I believe that there’s a balance to be struck between readability and succinctness. I think that the Ruby philosophy (or way) leans more towards the readability side of the spectrum. Check out Matz’s essay in the new O’Reilly book ‘Beautiful Code’ where he likens code to an essay. i.e. essays generally have messages, but the message is mostly meaningless unless it can be read and understood by other people. It’s a good read. Beautiful Code es un libro que tengo en casa en la cola de lectura, cuando le eche un vistazo os comento.
De todos modos, quién este interesado en este tipo de syntactic sugar también puede echarle un ojo a Array#to_proc – Nested properties for to_proc hack, que es una solución para el encadenamiento más legible y en la línea de Symbol#to_proc o bien New magical version of Symbol.to_proc que permite escribir algo como list.map &:to_i elegantemente como list.to_is con una extensión que sólo puede considerarse como “code hell”.
Extensiones al core de Ruby, pequeños pedazos de código que harán tu vida más sencilla. Hoy Enumerable.pluck y Enumerable.invoke
Publicado por el Martes, 16 de Octubre de 2007
En Trabe Soluciones vamos mejorando las librerías que utilizamos para desarrollar con Rails aplicación a aplicación. Puede decirse que somos como una esponja que va cogiendo buenas ideas de aquí y de allá. Hoy vamos a hablar de una par de extensiones al core de Ruby que utilizamos,
Primero el código:
module Enumerable
def invoke(method_name)
self.each { |o| o.send(method_name) }
end
def pluck(method_name)
self.collect { |o| o.send(method_name) }
end
end
Cómo podeís apreciar son unas sencillas funciones que solucionan dos tareas más que habituales cuando trabajamos con colecciones: invoke que invoca un método de cada elemento de un Enumerable y pluck que recolecta el resultado de invocar un método concreto sobre cada elemento de un Enumerable. Vamos, que abrevian dos construcciones típicas del lenguaje. Las hemos definido en el módulo Enumerable y no en la clase Array directamente para poder utilizarlas con cualquier clase que incluya este módulo (Array por ejemplo).
La inspiración de estas funciones se la debemos a Prototype, la famosa librería Javascript, que curiosamente se inspira a su vez en ruby: Most of Prototype’s idioms are influenced by Ruby, and those that have no analog in Ruby defer to existing or emerging web standards
Para terminar un ejemplo autoexplicativo de su utilidad.
# ANTES
def find_by_object_keys(objects)
find(:conditions => { :key => objects.collect { |o| o.key} }
end
# DESPUES
def find_by_object_keys(objects)
find(:conditions => { :key => objects.pluck(:key) }
end
Ejercicio para lectores avispados: Ahora sólo tenéis que modificar las funciones para que acepten parámetros para el método invocado. Seguramente lo necesitéis en algún momento.
Rails plugins (2): Annotate Models. Documenta tus modelos ActiveRecord automáticamente
Publicado por el Viernes, 12 de Octubre de 2007
El plugin que recomendamos hoy es annotate_models. Esta pequeña extensión ni modifica ni añade comportamiento a nuestras aplicaciones Rails y tampoco nos evita escribir código. No obstante, no debemos despreciar su valiosa ayuda.
annotate models añade una simpática tarea annotate_models al rakefile del proyecto que se ocupa de añadir al comienzo de los ficheros .rb que contienen modelos ActiveRecord un comentario con la información del schema. Esto es, nos anota los modelos con los atributos de la tabla de base de datos asociada al modelo. Veamos una anotación real como ejemplo:
# == Schema Information # Schema version: 34 # Annotated on: Wed Oct 10 10:38:58 +0200 2007 # # Table name: brands # # id :integer(11) not null, primary key # name_es :string(255) # name_en :string(255) # created_at :datetime not null # updated_at :datetime not null # image :string(255) # complete :boolean(1) # url :string(255) # margin :integer(11) #
Pequeño pero matón. Una gran ayuda mientras el soporte de los IDE de desarrollo como Netbeans o RadRails no mejore y autocompleten como Dios manda.
Sólo dos pegas (pequeñas, sí, pero ahi están). La primera: siempre anota los modelos, aun cuando no ha cambiado el esquema. Si trabajamos con un control de versiones (porque trabajamos con control de versiones ¿no?) el histórico de revisiones queda un pelín falseado, porque los ficheros han cambiado (esa línea Annotated on maldita), aunque no hayamos modificado el código.
La segunda: sólo anota los modelos de la carpeta model, cuando lo óptimo es que buscase por toda la carpeta del proyecto buscando subclases de ActiveRecord::Base. Si tenéis modelos en otra carpeta tendréis que modifcar el código fuente del plugin (que nadie se asuste, es bastante sencillo).
Resumiendo: anotad vuestros modelos
