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
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