En Ruby una clase puede incluir módulos. Al hacerlo podemos añadir métodos nuevos a la clase base, de forma que la clase termina con un montón de métodos nuevos que antes no tenía.

Sin embargo no es tan sencillo llevar esa ‘composición’ al nivel de métodos. Es fácil hacer que una clase añada nuevos métodos, pero cuando necesitas extender el comportamiento de un método es mas complicado.

Es una pared contra la que mis compañeros y yo chocamos al intentar implementar módulos en una aplicación en Ruby on Rails. Por ejemplo, si mi método view del controlador obtiene todos los objetos del modelo A, ¿cómo hago que también obtenga los objetos del modelo B cuando esté disponible otro módulo?

Investigando sobre ésto me topé con esta pregunta en StackOverflow: Dynamically extend existing method or override send method in ruby. Lo que me pareció interesante no fue la pregunta en sí, si no la respuesta de Sergio Tulentsev, que explica precisamente un método para realizar lo que yo estaba buscando.

Básicamente viene a decir que es posible realizar un alias del método original, después sobreescribirlo con un método nuevo, y en este nuevo método llamar al alias del original.

Así que me puse manos a la obra. La idea era realizar lo que propuso Sergio, pero de manera general, de forma que pudiera realizar eso mismo para el método que quisiera. La solución a la que llegué fue la siguiente.

class Module
  def extended_method_name(name, suffix)
    "__extended_#{name}$$#{suffix}".to_sym
  end

  def extended_methods_from(name)
    instance_methods.select do |method|
      method.to_s.split('$$')[0] == "__extended_#{name}"
    end
  end

  def extend_method(name)
    return unless instance_methods.include? name
    method_suffix = extended_methods_from(name).length
    method_name = extended_method_name(name, method_suffix)

    alias_method method_name, name

    define_method name do
      send(method_name)
      yield
    end
  end
end

Si mantenemos ese código que define nuevos métodos en la clase Module, podemos utilizar el método extend_method tanto en definiciones de clases como de módulos, de la siguiente forma:

module MoreBehaviour
  def self.included base
    base.class_eval do
      extend_method :work do
        puts "Lets do more work"
      end
    end
  end
end

module YetMoreBehaviour
  def self.included base
    base.class_eval do
      extend_method :work do
        puts "Doing yet more work"
      end
    end
  end
end

class MainClass
  def work
    puts "Doing some work"
  end

  include MoreBehaviour
  include YetMoreBehaviour
end

mc = MainClass.new
mc.work # Doing some work. Lets do more work. Doing yet more work.

En este ejemplo, hay una clase que define el método ‘work’. Los módulos utilizan el método ‘extend_method’ que definimos antes en la clase Module. Con ‘extend_method’ extienden el método ‘work’ de las clases en las que son incluidos.

Algunos comentarios sobre esto:

  • ‘extend_method’ solo funciona si el método que se quiere extender ya existe. En caso contrario será ignorado completamente.
  • Como puede verse en el ejemplo, se puede llamar ‘extend_method’ varias veces sobre un mismo método.
  • Se mantiene el orden de las extensiones de los métodos.

En ActiveSupport de Rails, existe una extensión tambien para la clase Module que implementa el método ‘alias_method_chain’. Este método también se podría utilizar para extender el comportamiento de un método, pero a mi parecer es más confuso. Os dejo con un enlace explicando el uso de alias_method_chain y otro a su implementación en active_support.

Por último, y como despedida, os dejo otro enlace con cuatro soluciones para extender el comportamiento de métodos en Ruby: http://blog.jayfields.com/2008/04/alternatives-for-redefining-methods.html

¡Buen día!