web-dev-qa-db-fra.com

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

Dites que je suis en train de patcher une méthode dans une classe, comment pourrais-je appeler la méthode surchargée à partir de la méthode surchargée? C'est à dire. Quelque chose un peu comme super

Par exemple.

class Foo
  def bar()
    "Hello"
  end
end 

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

>> Foo.new.bar == "Hello World"
415
James Hollingworth

EDIT: Cela fait 9 ans que j'ai écrit cette réponse et cela mérite une intervention chirurgicale esthétique pour rester à jour.

Vous pouvez voir la dernière version avant l'édition ici .


Vous ne pouvez pas appeler la méthode écrasée par son nom ou son mot-clé. C’est l’une des nombreuses raisons pour lesquelles il est préférable d’éviter les correctifs avec un singe et de préférer l’héritage, puisque vous pouvez appeler la méthode overridden.

Éviter les patches de singe

Héritage

Donc, si possible, préférez quelque chose comme ceci:

_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 objets Foo. Il suffit de changer chaque lieu qui crée un Foo pour créer plutôt un ExtendedFoo. Cela fonctionne encore mieux si vous utilisez Modèle de conception d'injection de dépendance , le Modèle de conception de méthode d'usine , le Modèle de conception de fabrique abstraite ou quelque chose du genre, car dans ce cas, il n’ya qu’un endroit où vous devez changer.

Délégation

Si vous ne contrôlez pas la création des objets Foo, par exemple parce qu’ils sont créés par une structure ne relevant pas de votre contrôle (comme Ruby-on-Rails pour exemple), vous pouvez utiliser le Wrapper Design Pattern :

_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 limite du système, où l’objet Foo entre dans votre code, vous l’enveloppez dans un autre objet, puis vous utilisez cet objet au lieu de l’objet original partout ailleurs dans votre code.

Ceci utilise la méthode d'assistance Object#DelegateClass de la bibliothèque delegate dans la bibliothèque stdlib.

"Nettoyer" le singe patcher

Module#prepend : Mixin en préparation

Les deux méthodes ci-dessus nécessitent de changer de système pour éviter les correctifs monkey. Cette section présente la méthode préférée et la moins invasive de correction de singe si le changement de système n'était 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 mélange directement dans le mixin 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 à propos de _Module#prepend_ dans cette question: Ruby module prepend vs derivation

Mixin Héritage (cassé)

J'ai vu certaines personnes essayer (et demander pourquoi cela ne fonctionne pas ici sur StackOverflow) quelque chose comme ceci, c'est-à-dire includeing un mixin au lieu de prepending:

_class Foo
  def bar
    'Hello'
  end
end 

module FooExtensions
  def bar
    super + ' World'
  end
end

class Foo
  include FooExtensions
end
_

Malheureusement, ça ne marchera pas. C’est une bonne idée, car elle utilise l’héritage, ce qui signifie que vous pouvez utiliser super. Cependant, Module#include insère le mixin above la classe dans la hiérarchie de l'héritage, ce qui signifie que _FooExtensions#bar_ ne sera jamais appelé (et si étaient appelés, le super ne ferait pas réellement référence à _Foo#bar_ mais plutôt à _Object#bar_ qui n'existe pas), puisque _Foo#bar_ sera toujours trouvé en premier.

Méthode d'emballage

La grande question est: comment pouvons-nous conserver la méthode bar sans réellement conserver une méthode réelle? Comme souvent, la réponse réside dans la programmation fonctionnelle. Nous récupérons la méthode en tant que objet réel, et nous utilisons une fermeture (c'est-à-dire un bloc) pour nous assurer que nous et seulement nous tenons 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: puisque _old_bar_ est juste une variable locale, elle sortira de la portée à la fin du corps de la classe et il est impossible d'y accéder de n'importe où, même en utilisant la réflexion! Et depuis Module#define_method prend un bloc et ferme des blocs sur leur environnement lexical environnant (qui est pourquoi nous utilisons _define_method_ au lieu de def ici), it (et only it) aura toujours accès à _old_bar_, même après sa sortie du champ d'application.

Brève explication:

_old_bar = instance_method(:bar)
_

Nous encapsulons ici la méthode bar dans un objet de méthode UnboundMethod et l’attribuons à la variable locale _old_bar_. Cela signifie que nous avons maintenant un moyen de conserver bar même après son écrasement.

_old_bar.bind(self)
_

C'est un peu délicat. Fondamentalement, dans Ruby (et dans à peu près tous les langages OO basés sur une distribution unique), une méthode est liée à un objet récepteur spécifique, appelé self dans Ruby. En d'autres termes: une méthode sait toujours sur quel objet elle a été appelée, elle sait ce que sa self est. Mais, nous avons récupéré la méthode directement à partir d’une classe. Comment sait-elle en quoi consiste son self?

Eh bien, ce n’est pas le cas, c’est pourquoi nous devons bind notre UnboundMethod à un objet d’abord, ce qui renverra un Method objet que nous pouvons ensuite appeler. (UnboundMethods ne peut pas être appelé, car ils ne savent pas quoi faire sans connaître leur self.)

Et que faisons-nous bind? Nous avons simplement bind pour nous-mêmes, de cette façon, il se comportera exactement comme l'original bar l'aurait!

Enfin, nous devons appeler le Method renvoyé par bind. Dans Ruby 1.9, il existe une nouvelle syntaxe astucieuse pour cela (.()), mais si vous êtes sur la 1.8, vous pouvez simplement utiliser la méthode call ; c’est ce que .() est traduit de toute façon.

Voici quelques autres questions, où certains de ces concepts sont expliqués:

“Sale” singe patcher

alias_method chaîne

Le problème que nous rencontrons avec notre patch de singe est que, lorsque nous écrasons la méthode, celle-ci est partie et nous ne pouvons plus l'appeler. Faisons donc 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, c’est que nous avons maintenant pollué l’espace de noms avec une méthode _old_bar_ superflue. Cette méthode apparaîtra dans notre documentation, elle apparaîtra dans la complétion de code dans nos IDE, elle apparaîtra pendant la réflexion. De plus, il peut toujours être appelé, mais on peut supposer que nous l'avons corrigé, parce que nous n'aimions pas son comportement au départ, de sorte que nous ne voudrions peut-être pas que d'autres personnes l'appellent.

Bien que cela présente des propriétés indésirables, il est malheureusement devenu populaire grâce à Module#alias_method_chain de AciveSupport.

Un aparté: Refinements

Dans le cas où vous n’auriez besoin que du comportement différent dans quelques emplacements spécifiques et non dans l’ensemble du système, vous pouvez utiliser Refinements pour limiter le patch monkey à une étendue spécifique. Je vais le démontrer ici en utilisant l'exemple _Module#prepend_ 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 de Refinements dans cette question: Comment activer le correctif de singe pour une méthode spécifique?


Idées abandonnées

Avant que la communauté Ruby ne se décide sur _Module#prepend_, il y avait de nombreuses idées différentes que vous pourriez parfois voir référencées dans des discussions plus anciennes. Tous ces éléments sont assimilés à _Module#prepend_.

Combinateurs de méthodes

Une idée a été l'idée des combinateurs de méthodes de CLOS. Il s’agit 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 seriez en mesure de "vous accrocher" à l'exécution de la méthode bar.

Toutefois, il n’est pas clair si et comment vous avez accès à la valeur de retour de bar dans _bar:after_. Peut-être pourrions-nous (ab) utiliser le mot clé super?

_class Foo
  def bar
    'Hello'
  end
end 

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

Remplacement

Le combinateur before équivaut à prepending un mixin avec une méthode prioritaire qui appelle super à la même end de la méthode. De même, le combinateur after équivaut à prepending un mixin avec une méthode de substitution qui appelle super à la même début de la méthode.

Vous pouvez également créer des éléments avant et après avoir appelé super, vous pouvez appeler super plusieurs fois et récupérer et manipuler la valeur de retour de super, 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 écrasée de la même manière que super vous permet d'appeler la méthode overridden:

_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 ceci est que c'est incompatible avec le passé: si vous avez une méthode appelée old, vous ne pourrez plus l'appeler!

Remplacement

super dans une méthode prioritaire dans un mixage prepended est essentiellement identique à old dans cette proposition.

redef mot-clé

Semblable à ce qui précède, mais au lieu d’ajouter un nouveau mot clé pour appeler la méthode écrasée et laisser def seul, nous ajoutons un nouveau mot clé pour redefining. Ceci est rétrocompatible, car la syntaxe est illégale pour le moment:

_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 super dans 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 à redéfinir la méthode dans un mixin prepended. super dans la méthode de substitution se comporte comme super ou old dans cette proposition.

1110
Jörg W Mittag

Jetez un coup d'œil aux méthodes d'aliasing, il s'agit en quelque sorte de renommer la méthode en un nouveau nom.

Pour plus d'informations et un point de départ, jetez un coup d'œil à ceci article sur les méthodes de remplacement (en particulier la première partie). Le documentation de l'API Ruby fournit également un exemple (moins élaboré).

12
Veger