ruby monkeypatching

ruby - Cuando Monkey está parcheando un método, ¿puede llamar al método anulado desde la nueva implementación?



monkeypatching (3)

Digamos que soy mono parcheando un método en una clase, ¿cómo podría llamar al método anulado desde el método de anulación? Es decir, algo como super

P.ej

class Foo def bar() "Hello" end end class Foo def bar() super() + " World" end end >> Foo.new.bar == "Hello World"


Eche un vistazo a los métodos de alias, esto es como cambiar el nombre del método a un nuevo nombre.

Para obtener más información y un punto de partida, eche un vistazo a este artículo sobre métodos de reemplazo (especialmente la primera parte). La documentación de la API de Ruby también proporciona un ejemplo (menos elaborado).


La clase que realizará la anulación debe volver a cargarse después de la clase que contiene el método original, así que solicítela en el archivo que realizará la anulación.


EDITAR : Han pasado 5 años desde que escribí originalmente esta respuesta, y merece algo de cirugía estética para mantenerla actualizada.

Puedes ver la última versión antes de la edición aquí .

No puede llamar al método sobrescrito por nombre o palabra clave. Esa es una de las muchas razones por las que se deben evitar los parches de los monos y se prefiere la herencia, ya que, obviamente, se puede llamar al método anulado .

Evitando parches de mono

Herencia

Entonces, si es posible, deberías preferir algo como esto:

class Foo def bar ''Hello'' end end class ExtendedFoo < Foo def bar super + '' World'' end end ExtendedFoo.new.bar # => ''Hello World''

Esto funciona, si controlas la creación de los objetos Foo . Simplemente cambia cada lugar que crea un Foo para crear un ExtendedFoo . Esto funciona incluso mejor si usa el Patrón de diseño de inyección de dependencia , el Patrón de diseño de método de fábrica , el Patrón de diseño de fábrica abstracto o algo parecido, porque en ese caso, solo hay un lugar que necesita cambiar.

Delegación

Si no controla la creación de los objetos Foo , por ejemplo, porque están creados por un marco que está fuera de su control (como ruby-on-rails por ejemplo), entonces podría usar el Patrón de diseño de envoltorio :

require ''delegate'' class Foo def bar ''Hello'' end end class WrappedFoo < DelegateClass(Foo) def initialize(wrapped_foo) super end def bar super + '' World'' end end foo = Foo.new # this is not actually in your code, it comes from somewhere else wrapped_foo = WrappedFoo.new(foo) # this is under your control wrapped_foo.bar # => ''Hello World''

Básicamente, en el límite del sistema, donde el objeto Foo entra en tu código, lo envuelves en otro objeto y luego usas ese objeto en lugar del original en cualquier otra parte de tu código.

Esto utiliza el método de ayuda Object#DelegateClass de la biblioteca de delegate en stdlib.

"Limpio" parches de mono

Module#prepend : Mixin Prepending

Los dos métodos anteriores requieren cambiar el sistema para evitar parches de mono. Esta sección muestra el método preferido y menos invasivo de parchear monos, en caso de que cambiar el sistema no sea una opción.

Module#prepend se agregó para admitir más o menos exactamente este caso de uso. Module#prepend hace lo mismo que Module#include , excepto que se mezcla en la mezcla directamente debajo de la clase:

class Foo def bar ''Hello'' end end module FooExtensions def bar super + '' World'' end end class Foo prepend FooExtensions end Foo.new.bar # => ''Hello World''

Nota: También escribí un poco sobre el Module#prepend en esta pregunta: Ruby módulo prepend vs derivación

Herencia Mixin (Rota)

He visto a algunas personas probar (y preguntar por qué no funciona aquí en ) algo como esto, es decir, include una mezcla en lugar de prepend :

class Foo def bar ''Hello'' end end module FooExtensions def bar super + '' World'' end end class Foo include FooExtensions end

Desafortunadamente, eso no funcionará. Es una buena idea, porque usa herencia, lo que significa que puedes usar super . Sin embargo, Module#include inserta el mixin sobre la clase en la jerarquía de herencia, lo que significa que FooExtensions#bar nunca se llamará (y si se llamara, el super no se referiría realmente a Foo#bar sino a la Object#bar que no existe), ya que Foo#bar siempre se encontrará primero.

Método de envoltura

La gran pregunta es: ¿cómo podemos aferrarnos al método de la bar , sin mantener realmente un método real ? La respuesta está, como lo hace a menudo, en la programación funcional. Conseguimos una retención del método como un objeto real, y usamos un cierre (es decir, un bloque) para asegurarnos de que nosotros y solo nosotros nos aferramos a ese objeto:

class Foo def bar ''Hello'' end end class Foo old_bar = instance_method(:bar) define_method(:bar) do old_bar.bind(self).() + '' World'' end end Foo.new.bar # => ''Hello World''

Esto está muy limpio: ya que old_bar es solo una variable local, quedará fuera del alcance al final del cuerpo de la clase, y es imposible acceder a él desde cualquier lugar, ¡ incluso utilizando la reflexión! Y dado que el Module#define_method toma un bloque, y los bloques se cierran sobre su entorno léxico circundante ( por lo que estamos usando define_method lugar de def aquí), (y solo él) todavía tendrá acceso a old_bar , incluso después de que haya salido de alcance.

Breve explicación:

old_bar = instance_method(:bar)

Aquí estamos envolviendo el método de la bar en un objeto del método UnboundMethod y asignándolo a la variable local old_bar . Esto significa que ahora tenemos una forma de mantener la bar incluso después de que se haya sobrescrito.

old_bar.bind(self)

Esto es un poco complicado. Básicamente, en Ruby (y en casi todos los lenguajes OO basados ​​en envío único), un método está vinculado a un objeto receptor específico, llamado self en Ruby. En otras palabras: un método siempre sabe qué objeto fue llamado, sabe cuál es su self . Pero, tomamos el método directamente de una clase, ¿cómo sabe lo que es?

Bueno, no es así, por lo que primero debemos bind nuestro UnboundMethod a un objeto, que devolverá un objeto Method que podemos llamar. ( UnboundMethod se puede llamar a UnboundMethod s, porque no saben qué hacer sin conocerse a self ).

¿Y a qué lo bind ? Simplemente lo bind a nosotros mismos, de esa manera se comportará exactamente como lo haría la bar original.

Por último, debemos llamar al Method que se devuelve desde bind . En Ruby 1.9, hay una nueva sintaxis ingeniosa para eso ( .() ), Pero si tiene 1.8, simplemente puede usar el método de call ; eso es lo que .() se traduce de todos modos.

Aquí hay un par de otras preguntas, donde se explican algunos de esos conceptos:

  • ¿Cómo puedo hacer referencia a una función en Ruby?
  • ¿Es el bloque de código de Ruby igual que la expresión lambda de C♯?

Parche de mono “sucio”

cadena alias_method

El problema que estamos teniendo con nuestro parche de monos es que cuando sobrescribimos el método, el método desaparece, por lo que no podemos llamarlo más. Entonces, hagamos una copia de respaldo!

class Foo def bar ''Hello'' end end class Foo alias_method :old_bar, :bar def bar old_bar + '' World'' end end Foo.new.bar # => ''Hello World'' Foo.new.old_bar # => ''Hello''

El problema con esto es que ahora hemos contaminado el espacio de nombres con un método old_bar superfluo. Este método se mostrará en nuestra documentación, se mostrará en la finalización del código en nuestros IDE, se mostrará durante la reflexión. Además, todavía se puede llamar, pero presumiblemente nos parchearon porque no nos gustó su comportamiento en primer lugar, por lo que es posible que no queramos que otras personas lo llamen.

A pesar de que esto tiene algunas propiedades indeseables, desafortunadamente se ha popularizado a través del Module#alias_method_chain .

Un lado: Refinamientos

En caso de que solo necesite el comportamiento diferente en algunos lugares específicos y no en todo el sistema, puede usar Refinamientos para restringir el parche de mono a un alcance específico. Voy a demostrarlo aquí usando el ejemplo del Module#prepend anterior al artículo anterior:

class Foo def bar ''Hello'' end end module ExtendedFoo module FooExtensions def bar super + '' World'' end end refine Foo do prepend FooExtensions end end Foo.new.bar # => ''Hello'' # We haven’t activated our Refinement yet! using ExtendedFoo # Activate our Refinement Foo.new.bar # => ''Hello World'' # There it is!

Puede ver un ejemplo más sofisticado del uso de Refinamientos en esta pregunta: ¿Cómo habilitar el parche de mono para un método específico?

Ideas abandonadas

Antes de que la comunidad de Ruby se Module#prepend el Module#prepend al Module#prepend , había varias ideas diferentes flotando alrededor de las que a veces se puede ver una referencia en discusiones anteriores. Todos estos son subsumidos por el Module#prepend .

Combinadores de métodos

Una idea fue la idea de los combinadores de métodos de CLOS. Esta es básicamente una versión muy ligera de un subconjunto de Programación Orientada a Aspectos.

Usando la sintaxis como

class Foo def bar:before # will always run before bar, when bar is called end def bar:after # will always run after bar, when bar is called # may or may not be able to access and/or change bar’s return value end end

Usted podría "enganchar" a la ejecución del método de bar .

Sin embargo, no está del todo claro si y cómo se obtiene acceso al valor de retorno de la bar:after dentro de la bar:after . Tal vez podríamos (ab) utilizar la palabra clave super ?

class Foo def bar ''Hello'' end end class Foo def bar:after super + '' World'' end end

Reemplazo

El combinador anterior es equivalente a prepend una mezcla con un método de reemplazo que se llama super al final del método. Del mismo modo, el combinador posterior es equivalente a prepend una mezcla con un método de reemplazo que se llama super al principio del método.

También puede hacer cosas antes y después de llamar a super , puede llamar a super varias veces, y ambos pueden recuperar y manipular el valor de retorno de super , lo que hace que el prepend más poderoso que los combinadores de métodos.

class Foo def bar:before # will always run before bar, when bar is called end end # is the same as module BarBefore def bar # will always run before bar, when bar is called super end end class Foo prepend BarBefore end

y

class Foo def bar:after # will always run after bar, when bar is called # may or may not be able to access and/or change bar’s return value end end # is the same as class BarAfter def bar original_return_value = super # will always run after bar, when bar is called # has access to and can change bar’s return value end end class Foo prepend BarAfter end

palabra clave old

Esta idea agrega una nueva palabra clave similar a super , que le permite llamar al método sobrescrito de la misma manera que super permite llamar al método sobrescrito :

class Foo def bar ''Hello'' end end class Foo def bar old + '' World'' end end Foo.new.bar # => ''Hello World''

El principal problema con esto es que es incompatible con versiones anteriores: si tiene un método llamado old , ¡ya no podrá llamarlo!

Reemplazo

super en un método de anulación en un mixin prepend es esencialmente lo mismo que old en esta propuesta.

palabra clave redef

Similar a lo anterior, pero en lugar de agregar una nueva palabra clave para llamar al método sobrescrito y dejar solo def , agregamos una nueva palabra clave para redefinir los métodos. Esto es compatible con versiones anteriores, ya que la sintaxis actualmente es ilegal de todos modos:

class Foo def bar ''Hello'' end end class Foo redef bar old + '' World'' end end Foo.new.bar # => ''Hello World''

En lugar de agregar dos palabras clave nuevas, también podríamos redefinir el significado de super inside redef :

class Foo def bar ''Hello'' end end class Foo redef bar super + '' World'' end end Foo.new.bar # => ''Hello World''

Reemplazo

redef un método equivale a anular el método en una redef . super en el método de anulación se comporta como super o old en esta propuesta.