Lorsque monkey corrige une méthode d'instance, pouvez-vous appeler la méthode remplacée à partir de la nouvelle implémentation?

444

Supposons que je corrige une méthode dans une classe, comment pourrais-je appeler la méthode substituée à partir de la méthode prioritaire? C'est à dire quelque chose un peu commesuper

Par exemple

class Foo
  def bar()
    "Hello"
  end
end 

class Foo
  def bar()
    super() + " World"
  end
end

>> Foo.new.bar == "Hello World"
James Hollingworth
la source
La première classe Foo ne devrait-elle pas en être une autre et la seconde Foo en hériterait-elle?
Draco Ater
1
non, je suis patch de singe. J'espérais qu'il y aurait quelque chose comme super () que je pourrais utiliser pour appeler la méthode originale
James Hollingworth
1
Cela est nécessaire lorsque vous ne contrôlez pas la création Foo et l'utilisation de Foo::bar. Vous devez donc patcher la méthode.
Halil Özgür

Réponses:

1166

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 Fooobjets. Il suffit de changer chaque endroit qui crée un Foopour 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 Fooobjets, par exemple parce qu'ils sont créés par un framework hors de votre contrôle (commepar 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' Fooobjet 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#DelegateClassméthode d'assistance de la delegatebibliothèque dans le stdlib.

Patch de singe «propre»

Module#prepend: Mixin Prepending

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#prependa été ajouté pour prendre en charge plus ou moins exactement ce cas d'utilisation. Module#prependfait 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#prependdans 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 includeing un mixin au lieu de prependle 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#includeinsère le mixin au - dessus de la classe dans la hiérarchie d'héritage, ce qui signifie qu'il FooExtensions#barne sera jamais appelé (et s'il était appelé, le superne se réfèrerait pas réellement à Foo#barmais Object#barn'existe pas), car il Foo#barsera toujours trouvé en premier.

Emballage de méthode

La grande question est: comment pouvons-nous conserver la barmé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_bars'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_methodprend un bloc, et que les blocs se referment sur leur environnement lexical environnant (c'est pourquoi nous utilisons à la define_methodplace d' defici), 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 barméthode dans un UnboundMethodobjet de méthode et l'affectons à la variable locale old_bar. Cela signifie que nous avons maintenant un moyen de conserver barmê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é selfen Ruby. En d'autres termes: une méthode sait toujours à quel objet elle a été appelée, elle sait ce qu'elle selfest. Mais, nous avons saisi la méthode directement dans une classe, comment sait-elle ce que selfc'est?

Eh bien, ce n'est pas le cas, c'est pourquoi nous devons d'abord bindaccéder UnboundMethodà un objet, qui renverra un Methodobjet que nous pourrons ensuite appeler. (Les UnboundMethodappels 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 bindpour nous-mêmes, de cette façon, il se comportera exactement comme l'original l' baraurait fait!

Enfin, nous devons appeler celui Methodqui 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 callmé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»

alias_method chaîne

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_barmé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.

Un aparté: raffinements

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#prependexemple 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 barméthode.

Il n'est cependant pas tout à fait clair si et comment vous pouvez accéder à barla valeur de retour de bar:after. Peut-être pourrions-nous (ab) utiliser le supermot-clé?

class Foo
  def bar
    'Hello'
  end
end 

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

Remplacement

Le combinateur avant est équivalent à prependun mixin avec une méthode prioritaire qui appelle superà la toute fin de la méthode. De même, le combinateur after est équivalent à prependun mixin avec une méthode prioritaire qui appelle superau tout début de la méthode.

Vous pouvez également faire des choses avant et après l'appel super, vous pouvez appeler superplusieurs fois et récupérer et manipuler superla valeur de retour de, ce qui rend prependplus 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 supervous 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

superdans une méthode prioritaire dans un prependmixage électronique est essentiellement le même que olddans 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 defseul, 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' superintérieur redef:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    super + ' World'
  end
end

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

Remplacement

redefining une méthode équivaut à remplacer la méthode dans un prependmixage ed. superdans la méthode prioritaire se comporte comme superou olddans cette proposition.

Jörg W Mittag
la source
@ Jörg W Mittag, est-ce que le thread d'approche de wrapping de méthode est sûr? Que se passe-t-il lorsque deux threads simultanés appellent bindla même old_methodvariable?
Harish Shetty
1
@KandadaBoggu: J'essaie de comprendre ce que vous entendez exactement par là :-) Cependant, je suis presque sûr qu'il n'est pas moins sûr pour les threads que tout autre type de métaprogrammation dans Ruby. En particulier, chaque appel à UnboundMethod#bindretournera un nouveau, différent Method, donc, je ne vois aucun conflit se produire, que vous l'appeliez deux fois de suite ou deux fois en même temps à partir de threads différents.
Jörg W Mittag
1
Je cherchais une explication sur le patch comme ça depuis que j'ai commencé sur le rubis et les rails. Très bonne réponse! La seule chose qui me manquait était une note sur class_eval vs la réouverture d'une classe. Le voici: stackoverflow.com/a/10304721/188462
Eugene
1
Ruby 2.0 a des améliorations blog.wyeworks.com/2012/8/3/ruby-refinements-landed-in-trunk
NARKOZ
5
Où trouvez-vous oldet redef? Mon 2.0.0 n'en a pas. Ah, il est difficile de ne pas manquer les autres idées concurrentes qui ne sont pas
entrées
12

Jetez un œil aux méthodes d'alias, c'est en quelque sorte renommer la méthode sous un nouveau nom.

Pour plus d'informations et un point de départ, consultez cet article sur les méthodes de remplacement (en particulier la première partie). La documentation de l'API Ruby fournit également un exemple (moins élaboré).

Veger
la source
-1

La classe qui fera la substitution doit être rechargée après la classe qui contient la méthode d'origine, donc require dans le fichier qui effectuera la substitution.

rplaurindo
la source