4Trabes Historias de una empresa en 100 metros cuadrados

El blog de Trabe Soluciones

Necesitas algo más flexible que Module#delegate de Rails, entonces Module#delegate_method

|

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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:

1
2
3
4
5
6
7
8
9
10
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:

1
2
3
4
5
6
7
8
9
10
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:

1
2
3
4
5
6
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 el method_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 nil y 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

1
2
3
4
5
6
7
8
9
10
11
12
13
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


Lo sentimos, pero los comentarios están cerrados