Cuando el mono parchea un método de instancia, ¿puede llamar al método anulado desde la nueva implementación?


443

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"

¿No debería ser la primera clase Foo otra y la segunda Foo heredar de ella?
Draco Ater

1
no, soy mono parcheando. Esperaba que hubiera algo como super () que pudiera usar para llamar al método original
James Hollingworth,

1
Esto es necesario cuando no controlas la creación Foo y el uso de Foo::bar. Así que tienes que parchear el método.
Halil Özgür

Respuestas:


1165

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:

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 Fooobjetos. Simplemente cambie cada lugar que crea un Foopara crear un ExtendedFoo. 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 Fooobjetos, por ejemplo, porque son creados por un marco que está fuera de su control (comopor 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 Fooobjeto 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#DelegateClassmétodo auxiliar de la delegatebiblioteca en stdlib.

Parches de mono "limpios"

Module#prepend: Mezcla previa

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 para el parche de monos, en caso de que cambiar el sistema no sea una opción.

Module#prependfue agregado para soportar más o menos exactamente este caso de uso. Module#prependhace lo mismo que Module#include, excepto que se 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#prependesta pregunta: el módulo Ruby antepuesto vs derivación

Herencia 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 de prependhacerlo:

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 usarla super. Sin embargo, Module#includeinserta el mixin por encima de la clase en la jerarquía de herencia, lo que significa que FooExtensions#barnunca será llamado (y si se llama, el superno habría en realidad se refieren a Foo#bar, sino más bien a Object#barque no existe), ya que Foo#barsiempre se encontrará en primer lugar.

Método de envoltura

La gran pregunta es: ¿cómo podemos aferrarnos al barmé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:

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: como old_bares 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 que Module#define_methodtoma un bloque y los bloques se cierran sobre su entorno léxico circundante ( por eso lo estamos usando en define_methodlugar de defaquí), él (y solo él) todavía tendrá acceso old_bar, incluso después de que haya salido del alcance.

Breve explicación:

old_bar = instance_method(:bar)

Aquí estamos envolviendo el barmétodo en un UnboundMethodobjeto de método y asignándolo a la variable local old_bar. Esto significa que ahora tenemos una forma de conservarlo barincluso 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 selfen Ruby. En otras palabras: un método siempre sabe en qué objeto fue llamado, sabe cuál selfes. Pero, tomamos el método directamente de una clase, ¿cómo sabe qué selfes?

Bueno, no es así, por eso necesitamos primero bindnuestro UnboundMethodobjeto, que devolverá un Methodobjeto que luego podamos llamar. ( UnboundMethodNo se pueden llamar s, porque no saben qué hacer sin saber su self).

¿Y para qué lo hacemos bind? Simplemente bindlo hacemos para nosotros, de esa manera se comportará exactamente como lo barhubiera hecho el original .

Por último, debemos llamar al Methodque se devuelve bind. En Ruby 1.9, hay una nueva sintaxis ingeniosa para eso ( .()), pero si está en 1.8, simplemente puede usar el callmé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 cadena

El 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!

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 old_barmé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#prependejemplo 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 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 por Module#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

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 barmétodo.

Sin embargo, no está del todo claro si y cómo obtiene acceso al barvalor de retorno interno bar:after. ¿Quizás podríamos (ab) usar la superpalabra clave?

class Foo
  def bar
    'Hello'
  end
end 

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

Reemplazo

El combinador before es equivalente a prependmezclar con un método de anulación que llama superal final del método. Del mismo modo, el combinador posterior es equivalente a prependmezclar con un método de anulación que llama superal principio del método.

También puede hacer cosas antes y después de llamar super, puede llamar supervarias veces, y recuperar y manipular superel valor de retorno, lo que lo hace prependmá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 al método sobrescrito de la misma manera que le superpermite llamar al método reemplazado :

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 ha llamado al método old, ¡ya no podrá llamarlo!

Reemplazo

superen un método primordial en un prepended mixin es esencialmente el mismo que olden esta propuesta.

redef palabra clave

Similar a lo anterior, pero en lugar de agregar una nueva palabra clave para llamar al método sobrescrito y dejarlo defsolo, 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 superinside redef:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    super + ' World'
  end
end

Foo.new.bar # => 'Hello World'

Reemplazo

redefLa introducción de un método es equivalente a la anulación del método en una prependcombinación. superen el método de anulación se comporta como supero olden esta propuesta.


@ Jörg W Mittag, ¿es seguro el hilo de enfoque de envoltura del método? ¿Qué sucede cuando dos hilos concurrentes llaman binda la misma old_methodvariable?
Harish Shetty

1
@KandadaBoggu: Estoy tratando de averiguar qué quieres decir exactamente con eso :-) Sin embargo, estoy bastante seguro de que no es menos seguro para subprocesos que cualquier otro tipo de metaprogramación en Ruby. En particular, cada llamada a UnboundMethod#binddevolverá un nuevo, diferente Method, 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.
Jörg W Mittag

1
Estaba buscando una explicación sobre parches como este desde que comencé con ruby ​​y rails. ¡Gran respuesta! Lo único que me faltaba era una nota sobre class_eval vs. reabrir una clase. Aquí está: stackoverflow.com/a/10304721/188462
Eugene


55
¿Dónde encuentras oldy redef? Mi 2.0.0 no los tiene. Ah, es difícil no perderse las otras ideas competidoras que no llegaron a Ruby fueron:
Nakilon

12

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).


-1

La clase que realizará la anulación debe volver a cargarse después de la clase que contiene el método original, por requirelo tanto , en el archivo que realizará la anulación.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.