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"
Author: Peter O., 2010-12-17

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:

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 superen 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 prepended 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 defalone, 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

redefining a method is equivalent to overriding the method in a prepended mixin. super en el el método de sobreescritura se comporta como super o old en esta propuesta.

 1058
Author: Jörg W Mittag,
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).

 12
Author: Veger,
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.

 0
Author: rplaurindo,
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