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 Foo
objetos. Simplemente cambie cada lugar que crea un Foo
para 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 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 :
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 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 la delegate
biblioteca en stdlib.
Parches de mono "limpios"
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#prepend
fue agregado para soportar más o menos exactamente este caso de uso. Module#prepend
hace 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#prepend
esta 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 prepend
hacerlo:
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#include
inserta el mixin por encima de la clase en la jerarquía de herencia, lo que significa que FooExtensions#bar
nunca será llamado (y si se llama, el super
no habría en realidad se refieren a Foo#bar
, sino más bien a Object#bar
que no existe), ya que Foo#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:
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_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 que Module#define_method
toma un bloque y los bloques se cierran sobre su entorno léxico circundante ( por eso lo estamos usando en define_method
lugar de def
aquí), é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 bar
método en un UnboundMethod
objeto de método y asignándolo a la variable local old_bar
. Esto significa que ahora tenemos una forma de conservarlo 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 otras palabras: un método siempre sabe en qué objeto fue llamado, sabe cuál self
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
nuestro UnboundMethod
objeto, que devolverá un Method
objeto que luego podamos llamar. ( UnboundMethod
No se pueden llamar s, porque no saben qué hacer sin saber su self
).
¿Y para qué lo hacemos bind
? Simplemente bind
lo hacemos para nosotros, de esa manera se comportará exactamente como lo bar
hubiera hecho el original .
Por último, debemos llamar al Method
que se devuelve bind
. En Ruby 1.9, hay una nueva sintaxis ingeniosa para eso ( .()
), pero si está en 1.8, simplemente puede usar el call
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"
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_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
.
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:
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 bar
método.
Sin embargo, no está del todo claro si y cómo obtiene acceso al bar
valor de retorno interno bar:after
. ¿Quizás podríamos (ab) usar la super
palabra clave?
class Foo
def bar
'Hello'
end
end
class Foo
def bar:after
super + ' World'
end
end
Reemplazo
El combinador before es equivalente a prepend
mezclar con un método de anulación que llama super
al final del método. Del mismo modo, el combinador posterior es equivalente a prepend
mezclar con un método de anulación que llama super
al principio del método.
También puede hacer cosas antes y después de llamar super
, puede llamar super
varias veces, y recuperar y manipular super
el valor de retorno, lo que lo hace 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
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 super
permite 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
super
en un método primordial en un prepend
ed mixin es esencialmente el mismo que old
en 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 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:
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
La introducción de un método es equivalente a la anulación del método en una prepend
combinación. super
en el método de anulación se comporta como super
o old
en esta propuesta.