¿Cuándo puedo o debo hacer uso de alias_method_chain?


Ruby Mag #26 is onlineEl primer encuentro que he tenido con alias_method_chain, es en la resolución de un problema con una gema dentro de un proyecto, en la que no me reconocía un adaptador, pero en mi caso, no me hacía falta que lo reconociera, sólo necesitaba que continuara sin hacer mucho más, un Monkey-patching o Freedom-patching es la solución, pero como todo no es bueno abusar de ello.

Pensé que era el único camino de su existencia y aplicación, pero me he vuelto a encontrar con él y explico en que otras circunstancias se debería utilizar. Existen casos en los que te puedes encontrar que la herencia y super no pueden hacer su trabajo y además no tenemos el control de la clase cuyo comportamiento estamos intentando de modificar. Esto dependerá de cómo lo hemos definido originalmente. ¿lo hemos definido en la propia clase, o en un módulo que se incluye en la clase?

module Reviews
   def review

      “My first review”

     end

end

class Product
   include Reviews
end

product = Product.new

product.review 

El resultado si lo probamos en consola, será:

 => “My first review” 

Si ahora lo hacemos a través de super:

module More      

            def review         

                  “#{super}-more and more review”      

            end    

end    

Product.send :include, More

product = Product.new

product.review 

Si lo probamos en consola, tendremos el resultado de que es capaz de saber su ancestro, cuando se llama a super y añadir nuestro segundo mensaje:

=> “My first review-more and more review”

Los resultados obtenidos hasta ahora son, lo que esperamos en cuanto a comportamiento. Vamos a cambiar un poco el entorno:

class Product

     def review

           “The review of the product”

     end

end

module More

     def review
          “#{super}-and more review of the second product”
     end
end

Product.send :include, More

product = Product.new

product.review

En este caso cuando lo ejecutamos en consola, obtenemos:

=> “The review of the product”

¿Por qué ahora obtenemos este resultado? No me lo esperaba así de primeras. Esto me ha llevado a una confusión en la que si no lo analizamos, nos perdemos. La razón está en que cuando estamos llamando a un método en un objeto, en nuestro caso estamos llamando a review del objeto instanciado product, si la clase de ese objeto contiene una definición de método que coincide con el que intentamos llamar, como es el caso, tenemos el método review cuando hacemos el include del módulo More (Product.send :include, More) y en la clase Product tenemos el método review, la resolución del ancestro de métodos se detiene justo cuando se encuentra el primero y el objeto entonces responde, pero si no lo encuentra, en ese caso, la llamada continúa con los ancestros del objeto. Por eso en este caso obtenemos como resultado => “The review of the product” del objeto Product y no del módulo More.

Por tanto en la primera forma, tenemos la pura herencia. En nuestro caso el objeto instanciado product hereda implícitamente de Object, por lo que se lleva todos los métodos definidos en objetos de forma automática. Si se hubiera utilizado la clase Product < MyOtherClass en nuestra definición de la clase en vez de un include, entonces habríamos heredado los métodos de la clase MyOtherClass, que a su vez podría heredar de otro objeto u otra super clase, y así podríamos hacerlo sucesivamente.

En la segunda forma tal como lo hemos planteado, es menos evidente cuando le echamos un vistazo, tenemos un send include que incluye un módulo que se inserta como el ancestro más inmediato de la clase, no respondiendo el método review definido en el módulo More ya que nuestra clase Product responde directamente el método review. ¿tenemos alguna solución para este caso? la respuesta es sí.

La solución es utilizar un encapsulamiento de alias_method_chain en Rails o alias_method en Ruby

En nuestro caso utilizamos un .class_eval y dentro del bloque los alias_method que necesitamos. También podríamos utilizar un .module_eval si fuese una jerarquía de módulos anidados donde tenemos los métodos. Imagina que tenemos una gema en la que queremos hacer un mokey-patching:

module SchemaPlus
    module ActiveRecord
        module ConnectionAdapters

           module AbstractAdapter
                  def included(base) 
                        base.alias_method_chain :initialize, :schema_plus
                  end

                          def initialize_with_schema_plus(*args) 
                               initialize_without_schema_plus(*args)

                               adapter = case adapter_name

                               …………………… more code ………………………………

                          end

           end

        end

     end

end

Ahora nuestro monkey-patching sería de la siguiente manera incluido en el directorio initializers:
SchemaPlus::ActiveRecord::ConnectionAdapters::AbstractAdapter.module_eval do
 
         def included(base) 
               base.alias_method_chain :initialize, :schema_plus
          end

Dejamos el método con una única línea el resto no la utilizamos ( es sólo un ejemplo para ver el caso de módulos anidados) 

def initialize_with_schema_plus(*args) 
        initialize_without_schema_plus(*args)

 end

end
El resultado es que sólo necesitamos que haga el método nuevo definido tal como se muestra en rojo.

Vemos la solución expuesta:

class Product

    def review

        “My review of the product”

    end

end

module More

     def self.included(base)
           base.class_eval do
               alias_method :review_without_more, :review
               alias_method :review, :review_with_more
           end
     end

     def review_with_more
          “#{review_without_more}-more review of my product”
     end
end

Product.send :include, More

product = Product.new

product.review 

product.class.ancestors

El resultado ahora después de aplicar el encapsulamiento es:

> product.review
=> “My review of the product-more review of my product”

> product.class.ancestors
=> [Product, More, Object, PP::ObjectMixin, JSON::Ext::Generator::GeneratorMethods::Object, ActiveSupport::Dependencies::Loadable, Kernel, BasicObject]

Ahora en el módulo More, se define el método def review_with_more y en la clase tenemos el método def review que los estamos mencionando a través de los alias_method o alias_method_chain, para que podamos hacer las llamadas directamente a review o a cada una de las formas definidas de _with_more o _without_more.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s