EDIT : Cela fait 9 ans que j'ai écrit cette réponse à l'origine, et cela mérite une chirurgie esthétique pour la garder à jour.
Vous pouvez voir la dernière version avant l'édition ici .
Vous ne pouvez pas appeler la méthode remplacée par nom ou mot-clé. C'est l'une des nombreuses raisons pour lesquelles le patch de singe doit être évité et l'héritage préféré à la place, car vous pouvez évidemment appeler la méthode substituée .
Éviter les patchs de singe
Héritage
Donc, si possible, vous devriez préférer quelque chose comme ça:
class Foo
def bar
'Hello'
end
end
class ExtendedFoo < Foo
def bar
super + ' World'
end
end
ExtendedFoo.new.bar # => 'Hello World'
Cela fonctionne, si vous contrôlez la création des Foo
objets. Il suffit de changer chaque endroit qui crée un Foo
pour en créer un ExtendedFoo
. Cela fonctionne encore mieux si vous utilisez le modèle de conception d'injection de dépendance , le modèle de conception de méthode d'usine , le modèle de conception d'usine abstraite ou quelque chose du genre, car dans ce cas, il n'y a qu'un seul endroit que vous devez modifier.
Délégation
Si vous ne contrôlez pas la création des Foo
objets, par exemple parce qu'ils sont créés par un framework hors de votre contrôle (commerubis sur railspar exemple), vous pouvez alors utiliser le modèle de conception Wrapper :
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'
Fondamentalement, à la frontière du système, où l' Foo
objet entre dans votre code, vous l'enveloppez dans un autre objet, puis utilisez cet objet au lieu de l'original partout ailleurs dans votre code.
Cela utilise la Object#DelegateClass
méthode d'assistance de la delegate
bibliothèque dans le stdlib.
Patch de singe «propre»
Les deux méthodes ci-dessus nécessitent de changer le système pour éviter les patches de singe. Cette section montre la méthode préférée et la moins invasive de correction de singe, si le changement du système n'est pas une option.
Module#prepend
a été ajouté pour prendre en charge plus ou moins exactement ce cas d'utilisation. Module#prepend
fait la même chose que Module#include
, sauf qu'il se mélange dans le mixin juste en dessous de la classe:
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'
Remarque: J'ai également écrit un peu Module#prepend
dans cette question: Ruby module prepend vs derivation
Héritage Mixin (cassé)
J'ai vu certaines personnes essayer (et demander pourquoi cela ne fonctionne pas ici sur StackOverflow) quelque chose comme ça, c'est-à-dire include
ing un mixin au lieu de prepend
le faire:
class Foo
def bar
'Hello'
end
end
module FooExtensions
def bar
super + ' World'
end
end
class Foo
include FooExtensions
end
Malheureusement, cela ne fonctionnera pas. C'est une bonne idée, car elle utilise l'héritage, ce qui signifie que vous pouvez l'utiliser super
. Cependant, Module#include
insère le mixin au - dessus de la classe dans la hiérarchie d'héritage, ce qui signifie qu'il FooExtensions#bar
ne sera jamais appelé (et s'il était appelé, le super
ne se réfèrerait pas réellement à Foo#bar
mais Object#bar
n'existe pas), car il Foo#bar
sera toujours trouvé en premier.
Emballage de méthode
La grande question est: comment pouvons-nous conserver la bar
méthode, sans vraiment garder une méthode réelle ? La réponse réside, comme c'est souvent le cas, dans la programmation fonctionnelle. Nous saisissons la méthode en tant qu'objet réel , et nous utilisons une fermeture (c'est-à-dire un bloc) pour nous assurer que nous et seulement nous nous accrochons à cet objet:
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'
C'est très propre: puisqu'il ne old_bar
s'agit que d'une variable locale, elle sera hors de portée à la fin du corps de classe, et il est impossible d'y accéder de n'importe où, même en utilisant la réflexion! Et puisque Module#define_method
prend un bloc, et que les blocs se referment sur leur environnement lexical environnant (c'est pourquoi nous utilisons à la define_method
place d' def
ici), il (et seulement lui) y aura toujours accès old_bar
, même après qu'il soit hors de portée.
Brève explication:
old_bar = instance_method(:bar)
Ici, nous enveloppons la bar
méthode dans un UnboundMethod
objet de méthode et l'affectons à la variable locale old_bar
. Cela signifie que nous avons maintenant un moyen de conserver bar
même après qu'il a été remplacé.
old_bar.bind(self)
C'est un peu délicat. Fondamentalement, dans Ruby (et dans presque tous les langages OO à répartition unique), une méthode est liée à un objet récepteur spécifique, appelé self
en Ruby. En d'autres termes: une méthode sait toujours à quel objet elle a été appelée, elle sait ce qu'elle self
est. Mais, nous avons saisi la méthode directement dans une classe, comment sait-elle ce que self
c'est?
Eh bien, ce n'est pas le cas, c'est pourquoi nous devons d'abord bind
accéder UnboundMethod
à un objet, qui renverra un Method
objet que nous pourrons ensuite appeler. (Les UnboundMethod
appels ne peuvent pas être appelés, car ils ne savent pas quoi faire sans connaître leur self
.)
Et que faisons-nous bind
? Nous le faisons simplement bind
pour nous-mêmes, de cette façon, il se comportera exactement comme l'original l' bar
aurait fait!
Enfin, nous devons appeler celui Method
qui est retourné bind
. Dans Ruby 1.9, il y a une nouvelle syntaxe astucieuse pour that ( .()
), mais si vous êtes sur 1.8, vous pouvez simplement utiliser la call
méthode; c'est ce qui .()
est traduit de toute façon.
Voici quelques autres questions, où certains de ces concepts sont expliqués:
Patch de singe «sale»
Le problème que nous rencontrons avec notre patch de singe est que lorsque nous remplaçons la méthode, la méthode disparaît, nous ne pouvons donc plus l'appeler. Alors, faisons juste une copie de sauvegarde!
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'
Le problème avec cela est que nous avons maintenant pollué l'espace de noms avec une old_bar
méthode superflue . Cette méthode apparaîtra dans notre documentation, elle apparaîtra dans l'achèvement du code dans nos IDE, elle apparaîtra pendant la réflexion. En outre, il peut toujours être appelé, mais on peut supposer que nous avons corrigé le singe, parce que nous n'aimions pas son comportement en premier lieu, donc nous pourrions ne pas vouloir que d'autres personnes l'appellent.
Malgré le fait que cela ait des propriétés indésirables, il est malheureusement devenu populaire grâce à AciveSupport Module#alias_method_chain
.
Dans le cas où vous n'avez besoin que du comportement différent dans quelques endroits spécifiques et non dans tout le système, vous pouvez utiliser des raffinements pour restreindre le patch singe à une portée spécifique. Je vais le démontrer ici en utilisant l' Module#prepend
exemple ci-dessus:
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!
Vous pouvez voir un exemple plus sophistiqué d'utilisation des raffinements dans cette question: Comment activer le patch singe pour une méthode spécifique?
Idées abandonnées
Avant que la communauté Ruby ne s'installe Module#prepend
, il y avait plusieurs idées différentes flottantes que vous pouvez parfois voir référencées dans des discussions plus anciennes. Tous ces éléments sont regroupés par Module#prepend
.
Combinateurs de méthodes
Une idée était l'idée de combinateurs de méthodes de CLOS. Il s'agit essentiellement d'une version très légère d'un sous-ensemble de programmation orientée aspect.
Utiliser une syntaxe comme
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
vous pourriez «vous accrocher» à l'exécution de la bar
méthode.
Il n'est cependant pas tout à fait clair si et comment vous pouvez accéder à bar
la valeur de retour de bar:after
. Peut-être pourrions-nous (ab) utiliser le super
mot-clé?
class Foo
def bar
'Hello'
end
end
class Foo
def bar:after
super + ' World'
end
end
Remplacement
Le combinateur avant est équivalent à prepend
un mixin avec une méthode prioritaire qui appelle super
à la toute fin de la méthode. De même, le combinateur after est équivalent à prepend
un mixin avec une méthode prioritaire qui appelle super
au tout début de la méthode.
Vous pouvez également faire des choses avant et après l'appel super
, vous pouvez appeler super
plusieurs fois et récupérer et manipuler super
la valeur de retour de, ce qui rend prepend
plus puissant que les combinateurs de méthodes.
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
et
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
mot-clé
Cette idée ajoute un nouveau mot-clé similaire à super
, qui vous permet d'appeler la méthode remplacée de la même manière super
vous permet d'appeler la méthode remplacée :
class Foo
def bar
'Hello'
end
end
class Foo
def bar
old + ' World'
end
end
Foo.new.bar # => 'Hello World'
Le principal problème avec cela est qu'il est incompatible en amont: si vous avez appelé la méthode old
, vous ne pourrez plus l'appeler!
Remplacement
super
dans une méthode prioritaire dans un prepend
mixage électronique est essentiellement le même que old
dans cette proposition.
redef
mot-clé
Semblable à ci-dessus, mais au lieu d'ajouter un nouveau mot clé pour appeler la méthode remplacée et de laisser def
seul, nous ajoutons un nouveau mot clé pour redéfinir les méthodes. Ceci est rétrocompatible, car la syntaxe est actuellement illégale de toute façon:
class Foo
def bar
'Hello'
end
end
class Foo
redef bar
old + ' World'
end
end
Foo.new.bar # => 'Hello World'
Au lieu d'ajouter deux nouveaux mots clés, nous pourrions également redéfinir la signification de l' super
intérieur redef
:
class Foo
def bar
'Hello'
end
end
class Foo
redef bar
super + ' World'
end
end
Foo.new.bar # => 'Hello World'
Remplacement
redef
ining une méthode équivaut à remplacer la méthode dans un prepend
mixage ed. super
dans la méthode prioritaire se comporte comme super
ou old
dans cette proposition.