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.