Cuando monkey parchea un método, ¿puede llamar al método anulado desde la nueva implementación?
Digamos que estoy parcheando un método en una clase, ¿cómo podría llamar al método sobreescrito desde el método sobreescrito? Es decir, algo un poco como super
Por ejemplo
class Foo
def bar()
"Hello"
end
end
class Foo
def bar()
super() + " World"
end
end
>> Foo.new.bar == "Hello World"
3 answers
EDIT: Han pasado 5 años desde que escribí originalmente esta respuesta, y merece un poco de cirugía estética para mantenerla actualizada.
Puede ver la última versión antes de la edición aquí.
No puedes llamar al método sobrescrito por nombre o palabra clave. Esa es una de las muchas razones por las que el parche de mono debe evitarse y la herencia debe preferirse en su lugar, ya que obviamente puede llamar al anulado método.
Evitando Parches de Monos
Herencia
Así que, si es posible, deberías preferir algo como esto: {[100]]}
class Foo
def bar
'Hello'
end
end
class ExtendedFoo < Foo
def bar
super + ' World'
end
end
ExtendedFoo.new.bar # => 'Hello World'
Esto funciona si controla la creación de los objetos Foo
. Simplemente cambie cada lugar que crea un Foo
para crear un ExtendedFoo
. Esto funciona aún mejor si utiliza el Patrón de Diseño de Inyección de Dependencias , el Patrón de Diseño del Método de Fábrica , el Patrón de Diseño Abstracto de Fábrica o algo en esa línea, porque en ese caso, solo hay lugar que necesita cambiar.
Delegación
Si no controla la creación de los objetos Foo
, por ejemplo porque son creados por un framework que está fuera de su control (como ruby-on-rails por ejemplo), entonces podría usar el Patrón de Diseño de Envoltura :
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 su código, lo envuelves en otro objeto, y luego usas ese objeto en lugar del original en cualquier otro lugar de tu código.
Esto utiliza la Object#DelegateClass
método de ayuda de la delegate
biblioteca en el stdlib.
Parche de mono"Limpio"
Module#prepend
: Mixin Prepending
Los dos métodos anteriores requieren cambiar el sistema para evitar el parcheo del mono. Esta sección muestra el método preferido y menos invasivo de patching mono, debe cambiar el sistema no es una opción.
Module#prepend
fue añadido para soportar más o menos exactamente este caso de uso. Module#prepend
hace lo mismo que Module#include
, excepto que mezcla en el mixin 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 Module#prepend
en esta pregunta: El módulo Ruby antepone vs derivación
Herencia Mixin (roto)
He visto a algunas personas intentarlo (y preguntar por qué no funciona aquí en StackOverflow) algo como esto, es decir, include
ing a mixin en lugar de prepend
ing it:
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 arriba la clase en la jerarquía de herencia, lo que significa que FooExtensions#bar
nunca será llamado (y si de llamada, el super
en realidad no se refieren a Foo#bar
sino a 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 bar
, sin realmente mantener alrededor de un método real? La respuesta se encuentra, como lo hace tan a menudo, en la programación funcional. Obtenemos un control del método como un objeto real , y usamos un cierre (es decir, un bloque) para asegurarnos de que y solo nos aferramos a eso 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 es muy limpio: dado que old_bar
es solo una variable local, saldrá fuera de alcance al final del cuerpo de la clase, y es imposible acceder a ella desde cualquier lugar, incluso usando reflexión! Y desde Module#define_method
toma un bloque, y los bloques se cierran sobre su entorno léxico circundante (que es por qué estamos usando define_method
en lugar de def
aquí), it (y solo it) todavía tendrá acceso a old_bar
, incluso después de se ha salido del alcance.
Breve explicación:
old_bar = instance_method(:bar)
Aquí estamos envolviendo el método bar
en un UnboundMethod
objeto método y asignándolo a la variable local old_bar
. Esto significa que ahora tenemos una manera de aferrarnos a 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 un solo despacho), un método está vinculado a un objeto receptor específico, llamado self
en Ruby. En en otras palabras: un método siempre sabe a 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 su self
?
Bueno, no lo hace, por lo que necesitamos bind
nuestro UnboundMethod
a un objeto primero, que devolverá un Method
objeto que luego podemos llamar. (UnboundMethod
s no pueden ser llamados, porque no saben qué hacer sin conocer su self
.)
¿Y para qué lo hacemos bind
? Nos simplemente bind
a nosotros mismos, de esa manera se comportará exactamente como el original bar
tendría!
Por último, tenemos que llamar a la Method
que se devuelve de bind
. En Ruby 1.9, hay una nueva sintaxis ingeniosa para eso (.()
), pero si estás en 1.8, simplemente puedes usar call
método; eso es a lo que .()
se traduce de todos modos.
Aquí hay un par de otras preguntas, donde algunos de esos conceptos son explained:
- ¿Cómo hago referencia a una función en Ruby?
- ¿El bloque de código de Ruby es el mismo que la expresión lambda de C??
Parche de Mono"Sucio"
alias_method
cadena
El problema que estamos teniendo con nuestro parche de mono es que cuando sobrescribimos el método, el método se ha ido, por lo que no podemos llamarlo más. Por lo tanto, vamos a hacer una copia de seguridad!
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 han contaminado el espacio de nombres con un superfluo método old_bar
. Este método se mostrará en nuestra documentación, se mostrará en la finalización del código en nuestros IDEs, se mostrará durante la reflexión. Además, todavía se puede llamar, pero presumiblemente lo parcheamos, porque no nos gustó su comportamiento en primer lugar, por lo que puede que no queramos que otras personas lo llamen.
A pesar del hecho de que esto tiene algunas propiedades indeseables, desafortunadamente se ha popularizado a través de Acivesupport's Module#alias_method_chain
.
Un aparte: Refinamientos
En caso de que solo necesite el comportamiento diferente en unos pocos lugares específicos y no en todo el sistema, puede usar Mejoras para restringir el parche monkey a un ámbito específico. Voy a demostrarlo aquí usando el ejemplo Module#prepend
de arriba:
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 monkey patch para método específico?
Ideas abandonadas
Antes de que la comunidad de Ruby se decidiera por Module#prepend
, había múltiples ideas diferentes flotando alrededor que ocasionalmente se pueden ver referenciadas en discusiones más antiguas. Todos ellos están subsumidos por Module#prepend
.
Combinadores de métodos
Una idea fue la idea de 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 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 "engancharse" a la ejecución del método bar
.
Sin embargo, no está muy claro si y cómo obtiene acceso al valor de retorno de bar
dentro de bar:after
. Tal vez podríamos (ab)usar la palabra clave super
?
class Foo
def bar
'Hello'
end
end
class Foo
def bar:after
super + ' World'
end
end
Sustitución
El combinador before es equivalente a prepend
ing un mixin con un método overriding que llama super
al final del método. Del mismo modo, el combinador después es equivalente a prepend
ing un mixin con un método overriding que llama super
en el mismo que comienza del método.
También puedes hacer cosas antes de y después de llamar a super
, puedes llamar a super
varias veces, y tanto recuperar como manipular el valor de retorno de super
, haciendo que prepend
sea 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
old
palabra clave
Esta idea agrega una nueva palabra clave similar a super
, que le permite llamar a la sobrescribe método de la misma manera super
le permite llamar a la reemplaza método:
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 hacia atrás: si tiene un método llamado old
, ¡ya no podrá llamarlo!
Sustitución
super
en un método de reemplazo en un prepend
ed mixin es esencialmente el mismo que old
en esta propuesta.
redef
palabra clave
Similar a la anterior, pero en lugar de agregar una nueva keyword for calling the overwritten method and leaving def
alone, agregamos una nueva palabra clave para redefiniendo 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 nuevas palabras clave, también podríamos redefinir el significado de super
dentro de redef
:
class Foo
def bar
'Hello'
end
end
class Foo
redef bar
super + ' World'
end
end
Foo.new.bar # => 'Hello World'
Sustitución
redef
ining a method is equivalent to overriding the method in a prepend
ed mixin. super
en el el método de sobreescritura se comporta como super
o old
en esta propuesta.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-12-11 07:25:02
Eche un vistazo a los métodos de aliasing, 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 de sustitución de métodos (especialmente la primera parte). El Ruby API docs , también proporciona (un ejemplo menos elaborado).
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-12-17 11:52:50
La clase que hará override debe ser recargada después de la clase que contiene el método original, por lo que require
en el archivo que hará override.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-04-01 00:03:50