Digamos que soy mono parcheando un método en una clase, ¿cómo podría llamar al método anulado desde el método anulado? Es decir, algo parecidosuper
P.ej
class Foo
def bar()
"Hello"
end
end
class Foo
def bar()
super() + " World"
end
end
>> Foo.new.bar == "Hello World"
ruby
monkeypatching
James Hollingworth
fuente
fuente
Foo
y el uso deFoo::bar
. Así que tienes que parchear el método.Respuestas:
EDITAR : Han pasado 9 años desde que escribí originalmente esta respuesta, y merece una 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 mono y preferir la herencia, ya que obviamente se puede llamar al método anulado .
Evitar parches de mono
Herencia
Entonces, si es posible, debe preferir algo como esto:
Esto funciona si controlas la creación de los
Foo
objetos. Simplemente cambie cada lugar que crea unFoo
para crear unExtendedFoo
. Esto funciona incluso mejor si se utiliza la dependencia del diseño del modelo de inyección , el patrón Factory Method diseño , el patrón Resumen de diseño de fábrica o algo por el estilo, porque en ese caso, sólo hay lugar que necesita para el cambio.Delegación
Si no controla la creación de los
Foo
objetos, por ejemplo, porque son creados por un marco que está fuera de su control (comoRuby on Railspor ejemplo), entonces podría usar el Patrón de diseño de envoltura :Básicamente, en el límite del sistema, donde el
Foo
objeto entra en su código, lo envuelve en otro objeto y luego usa ese objeto en lugar del original en cualquier otro lugar de su código.Esto utiliza el
Object#DelegateClass
método auxiliar de ladelegate
biblioteca en stdlib.Parches de mono "limpios"
Module#prepend
: Mezcla previaLos dos métodos anteriores requieren cambiar el sistema para evitar parches de mono. Esta sección muestra el método preferido y menos invasivo para el parche de monos, en caso de que cambiar el sistema no sea una opción.
Module#prepend
fue agregado para soportar más o menos exactamente este caso de uso.Module#prepend
hace lo mismo queModule#include
, excepto que se mezcla en el mixin directamente debajo de la clase:Nota: También escribí un poco sobre
Module#prepend
esta pregunta: el módulo Ruby antepuesto vs derivaciónHerencia Mixin (rota)
He visto a algunas personas probar (y preguntar por qué no funciona aquí en StackOverflow) algo como esto, es decir
include
, mezclar en lugar deprepend
hacerlo:Desafortunadamente, eso no funcionará. Es una buena idea, porque usa herencia, lo que significa que puedes usarla
super
. Sin embargo,Module#include
inserta el mixin por encima de la clase en la jerarquía de herencia, lo que significa queFooExtensions#bar
nunca será llamado (y si se llama, elsuper
no habría en realidad se refieren aFoo#bar
, sino más bien aObject#bar
que no existe), ya queFoo#bar
siempre se encontrará en primer lugar.Método de envoltura
La gran pregunta es: ¿cómo podemos aferrarnos al
bar
método, sin realmente mantener un método real ? La respuesta radica, como suele suceder, en la programación funcional. Obtenemos el método como un objeto real , y usamos un cierre (es decir, un bloque) para asegurarnos de que nosotros y solo nosotros nos aferremos a ese objeto:Esto está muy limpio: como
old_bar
es solo una variable local, se saldrá del alcance al final del cuerpo de la clase, y es imposible acceder a él desde cualquier lugar, ¡ incluso usando la reflexión! Y dado queModule#define_method
toma un bloque y los bloques se cierran sobre su entorno léxico circundante ( por eso lo estamos usando endefine_method
lugar dedef
aquí), él (y solo él) todavía tendrá accesoold_bar
, incluso después de que haya salido del alcance.Breve explicación:
Aquí estamos envolviendo el
bar
método en unUnboundMethod
objeto de método y asignándolo a la variable localold_bar
. Esto significa que ahora tenemos una forma de conservarlobar
incluso después de que se haya sobrescrito.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 otras palabras: un método siempre sabe en qué objeto fue llamado, sabe cuálself
es. Pero, tomamos el método directamente de una clase, ¿cómo sabe quéself
es?Bueno, no es así, por eso necesitamos primero
bind
nuestroUnboundMethod
objeto, que devolverá unMethod
objeto que luego podamos llamar. (UnboundMethod
No se pueden llamar s, porque no saben qué hacer sin saber suself
).¿Y para qué lo hacemos
bind
? Simplementebind
lo hacemos para nosotros, de esa manera se comportará exactamente como lobar
hubiera hecho el original .Por último, debemos llamar al
Method
que se devuelvebind
. En Ruby 1.9, hay una nueva sintaxis ingeniosa para eso (.()
), pero si está en 1.8, simplemente puede usar elcall
método; a eso es a lo que.()
se traduce de todos modos.Aquí hay un par de otras preguntas, donde se explican algunos de esos conceptos:
Parches de mono "sucio"
alias_method
cadenaEl problema que tenemos con nuestro parche de mono es que cuando sobrescribimos el método, el método desaparece, por lo que ya no podemos llamarlo. Entonces, ¡hagamos una copia de seguridad!
El problema con esto es que ahora hemos contaminado el espacio de nombres con un
old_bar
método superfluo . Este método aparecerá en nuestra documentación, se mostrará al completar el código en nuestros IDEs, se mostrará durante la reflexión. Además, todavía se puede llamar, pero presumiblemente lo hemos parcheado, 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 tiene algunas propiedades indeseables, desafortunadamente se popularizó a través de AciveSupport's
Module#alias_method_chain
.Un aparte: 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
Module#prepend
ejemplo de arriba: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 estableciera
Module#prepend
, había varias ideas diferentes flotando alrededor que ocasionalmente puede ver referenciadas en discusiones anteriores. Todos estos están subsumidos porModule#prepend
.Método Combinadores
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 sintaxis como
usted podría "engancharse" a la ejecución del
bar
método.Sin embargo, no está del todo claro si y cómo obtiene acceso al
bar
valor de retorno internobar:after
. ¿Quizás podríamos (ab) usar lasuper
palabra clave?Reemplazo
El combinador before es equivalente a
prepend
mezclar con un método de anulación que llamasuper
al final del método. Del mismo modo, el combinador posterior es equivalente aprepend
mezclar con un método de anulación que llamasuper
al principio del método.También puede hacer cosas antes y después de llamar
super
, puede llamarsuper
varias veces, y recuperar y manipularsuper
el valor de retorno, lo que lo haceprepend
más poderoso que los combinadores de métodos.y
old
palabra claveEsta idea agrega una nueva palabra clave similar a
super
, que le permite llamar al método sobrescrito de la misma manera que lesuper
permite llamar al método reemplazado :El principal problema con esto es que es incompatible con versiones anteriores: si ha llamado al método
old
, ¡ya no podrá llamarlo!Reemplazo
super
en un método primordial en unprepend
ed mixin es esencialmente el mismo queold
en esta propuesta.redef
palabra claveSimilar a lo anterior, pero en lugar de agregar una nueva palabra clave para llamar al método sobrescrito y dejarlo
def
solo, 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:En lugar de agregar dos palabras clave nuevas, también podríamos redefinir el significado de
super
insideredef
:Reemplazo
redef
La introducción de un método es equivalente a la anulación del método en unaprepend
combinación.super
en el método de anulación se comporta comosuper
oold
en esta propuesta.fuente
bind
a la mismaold_method
variable?UnboundMethod#bind
devolverá un nuevo, diferenteMethod
, por lo tanto, no veo surgir ningún conflicto, independientemente de si lo llama dos veces seguidas o dos veces al mismo tiempo desde diferentes hilos.old
yredef
? Mi 2.0.0 no los tiene. Ah, es difícil no perderse las otras ideas competidoras que no llegaron a Ruby fueron: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, consulte este artículo sobre métodos de reemplazo (especialmente la primera parte). Los documentos de Ruby API también proporcionan (un ejemplo menos elaborado).
fuente
La clase que realizará la anulación debe volver a cargarse después de la clase que contiene el método original, por
require
lo tanto , en el archivo que realizará la anulación.fuente