Können Sie beim Patchen einer Instanzmethode durch Affen die überschriebene Methode aus der neuen Implementierung aufrufen?


442

Angenommen, ich bin ein Affe, der eine Methode in einer Klasse patcht. Wie kann ich die überschriebene Methode von der überschreibenden Methode aus aufrufen? Dh so etwas wiesuper

Z.B

class Foo
  def bar()
    "Hello"
  end
end 

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

>> Foo.new.bar == "Hello World"

Sollte die erste Foo-Klasse nicht eine andere sein und die zweite Foo davon erben?
Draco Ater

1
Nein, ich bin Affe flicken. Ich hatte gehofft, dass es so etwas wie super () geben würde, mit dem ich die ursprüngliche Methode aufrufen könnte
James Hollingworth

1
Dies ist erforderlich, wenn Sie die Erstellung Foo und Verwendung von nicht steuern Foo::bar. Sie müssen also die Methode mit einem Affen-Patch versehen .
Halil Özgür

Antworten:


1165

EDIT : Es ist 9 Jahre her, seit ich diese Antwort ursprünglich geschrieben habe, und es verdient eine Schönheitsoperation, um sie auf dem neuesten Stand zu halten.

Sie können die letzte Version vor der Bearbeitung hier sehen .


Sie können die überschriebene Methode nicht über Name oder Schlüsselwort aufrufen . Dies ist einer der vielen Gründe, warum das Patchen von Affen vermieden und stattdessen die Vererbung bevorzugt werden sollte, da Sie natürlich die überschriebene Methode aufrufen können .

Vermeiden von Affen-Patches

Erbe

Wenn möglich, sollten Sie so etwas bevorzugen:

class Foo
  def bar
    'Hello'
  end
end 

class ExtendedFoo < Foo
  def bar
    super + ' World'
  end
end

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

Dies funktioniert, wenn Sie die Erstellung der FooObjekte steuern . Ändern Sie einfach jeden Ort, der ein erstellt, Fooum stattdessen ein zu erstellen ExtendedFoo. Dies funktioniert sogar noch besser, wenn Sie das Entwurfsmuster für Abhängigkeitsinjektionen , das Entwurfsmuster für Factory-Methoden , das Entwurfsmuster für abstrakte Fabriken oder ähnliches verwenden, da in diesem Fall nur der Ort geändert werden muss.

Delegation

Wenn Sie die Erstellung der FooObjekte nicht steuern , z. B. weil sie von einem Framework erstellt werden, das außerhalb Ihrer Kontrolle liegt (zZum Beispiel), dann könnten Sie das Wrapper Design Pattern verwenden :

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'

Grundsätzlich Foowickeln Sie das Objekt an der Grenze des Systems, an der es in Ihren Code eingeht, in ein anderes Objekt ein und verwenden dieses Objekt dann anstelle des ursprünglichen Objekts überall in Ihrem Code.

Dies verwendet die Hilfsmethode Object#DelegateClassaus der delegateBibliothek in der stdlib.

"Clean" Monkey Patching

Module#prepend: Mixin Prepending

Bei den beiden oben genannten Methoden muss das System geändert werden, um das Patchen von Affen zu vermeiden. Dieser Abschnitt zeigt die bevorzugte und am wenigsten invasive Methode zum Patchen von Affen, falls ein Systemwechsel nicht möglich ist.

Module#prependwurde hinzugefügt, um mehr oder weniger genau diesen Anwendungsfall zu unterstützen. Module#prependmacht das Gleiche wie Module#include, außer dass es sich im Mixin direkt unter der Klasse mischt :

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'

Hinweis: Ich habe Module#prependin dieser Frage auch ein wenig darüber geschrieben : Ruby-Modul vorangestellt gegen Ableitung

Mixin-Vererbung (gebrochen)

Ich habe einige Leute gesehen, die versucht haben (und gefragt haben, warum es hier bei StackOverflow nicht funktioniert), so etwas, dh includeein Mixin anstatt prependes zu machen:

class Foo
  def bar
    'Hello'
  end
end 

module FooExtensions
  def bar
    super + ' World'
  end
end

class Foo
  include FooExtensions
end

Das wird leider nicht funktionieren. Es ist eine gute Idee, weil es Vererbung verwendet, was bedeutet, dass Sie verwenden können super. Allerdings Module#includefügt die mixin über die Klasse in der Vererbungshierarchie, die Mittel , die FooExtensions#barnie aufgerufen werden (und wenn es wurden genannt, die supernicht tatsächlich beziehen sich auf , Foo#barsondern auf Object#bardie es nicht gibt), da Foo#barimmer zuerst gefunden werden.

Methodenverpackung

Die große Frage ist: Wie können wir an der barMethode festhalten , ohne tatsächlich an einer tatsächlichen Methode festzuhalten ? Die Antwort liegt wie so oft in der funktionalen Programmierung. Wir erhalten die Methode als tatsächliches Objekt und verwenden einen Abschluss (dh einen Block), um sicherzustellen, dass wir und nur wir an diesem Objekt festhalten:

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'

Dies ist sehr sauber: Da old_bares sich nur um eine lokale Variable handelt, wird sie am Ende des Klassenkörpers nicht mehr gültig sein, und es ist unmöglich, von überall darauf zuzugreifen, selbst wenn Reflektion verwendet wird! Und da Module#define_methodein Block benötigt wird und Blöcke über der umgebenden lexikalischen Umgebung geschlossen werden ( weshalb wir define_methodstatt defhier verwenden), hat er (und nur er) weiterhin Zugriff darauf old_bar, selbst nachdem er den Gültigkeitsbereich verlassen hat.

Kurze Erklärung:

old_bar = instance_method(:bar)

Hier verpacken wir die barMethode in ein UnboundMethodMethodenobjekt und weisen es der lokalen Variablen zu old_bar. Das heißt, wir haben jetzt die Möglichkeit, barauch nach dem Überschreiben daran festzuhalten .

old_bar.bind(self)

Das ist etwas knifflig. Grundsätzlich ist in Ruby (und in nahezu allen Single-Dispatch-basierten OO-Sprachen) eine Methode an ein bestimmtes Empfängerobjekt gebunden, das selfin Ruby aufgerufen wird . Mit anderen Worten: Eine Methode weiß immer, auf welchem ​​Objekt sie aufgerufen wurde, sie weiß, was es selfist. Aber wir haben die Methode direkt aus einer Klasse geholt. Woher weiß sie, was sie selfist?

Nun, es funktioniert nicht, weshalb wir brauchen bindunsere UnboundMethodauf ein Objekt zuerst, das ein zurückkehren MethodObjekt , das wir dann anrufen. ( UnboundMethods können nicht angerufen werden, weil sie nicht wissen, was sie tun sollen, ohne ihre zu kennen self.)

Und wozu machen wir binddas? Wir haben es einfach bindfür uns, so wird es sich genau so verhalten wie das Original bar!

Zuletzt müssen wir das zurückrufen, von dem Methodzurückgegeben wird bind. In Ruby 1.9 gibt es dafür eine raffinierte neue Syntax ( .()), aber wenn Sie 1.8 verwenden, können Sie einfach die callMethode verwenden. das .()wird sowieso übersetzt.

Hier sind einige andere Fragen, in denen einige dieser Konzepte erläutert werden:

"Dirty" Monkey Patching

alias_method Kette

Das Problem, das wir mit dem Patchen von Affen haben, ist, dass die Methode beim Überschreiben der Methode weg ist und wir sie nicht mehr aufrufen können. Machen wir also einfach eine Sicherungskopie!

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'

Das Problem dabei ist, dass wir den Namespace jetzt mit einer überflüssigen old_barMethode verschmutzt haben . Diese Methode wird in unserer Dokumentation angezeigt, sie wird bei der Code-Vervollständigung in unseren IDEs angezeigt und wird während der Reflexion angezeigt. Es kann auch immer noch aufgerufen werden, aber vermutlich haben wir es mit Affen gepatcht, weil uns sein Verhalten an erster Stelle nicht gefallen hat, sodass wir möglicherweise nicht möchten, dass andere Leute es anrufen.

Trotz der Tatsache, dass dies einige unerwünschte Eigenschaften hat, ist es leider durch AciveSupport's populär geworden Module#alias_method_chain.

Nebenbei: Verfeinerungen

Falls Sie das unterschiedliche Verhalten nur an bestimmten Stellen und nicht im gesamten System benötigen, können Sie mithilfe von Verfeinerungen den Affen-Patch auf einen bestimmten Bereich beschränken. Ich werde es hier anhand des Module#prependobigen Beispiels demonstrieren :

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!

In dieser Frage sehen Sie ein komplexeres Beispiel für die Verwendung von Verfeinerungen: Wie aktiviere ich den Affen-Patch für eine bestimmte Methode?


Verlassene Ideen

Bevor sich die Ruby-Community niederließ Module#prepend, gab es mehrere verschiedene Ideen, auf die gelegentlich in älteren Diskussionen verwiesen wird. All dies wird von zusammengefasst Module#prepend.

Methodenkombinatoren

Eine Idee war die Idee von Methodenkombinatoren von CLOS. Dies ist im Grunde eine sehr leichte Version einer Teilmenge der aspektorientierten Programmierung.

Mit Syntax wie

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

Sie könnten sich in die Ausführung der barMethode "einhaken" .

Es ist jedoch nicht ganz klar, ob und wie Sie Zugriff auf barden Rückgabewert von erhalten bar:after. Vielleicht könnten wir das superSchlüsselwort (ab) verwenden ?

class Foo
  def bar
    'Hello'
  end
end 

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

Ersatz

Der Vorher-Kombinator entspricht prependeinem Mixin mit einer überschreibenden Methode, superdie ganz am Ende der Methode aufgerufen wird. Ebenso entspricht der After-Combinator prependeinem Mixin mit einer überschreibenden Methode, superdie ganz am Anfang der Methode aufgerufen wird.

Sie können auch Dinge vor und nach dem Aufruf erledigen super, Sie können supermehrmals aufrufen und superden Rückgabewert sowohl abrufen als auch bearbeiten , was prependleistungsfähiger ist als Methodenkombinatoren.

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

und

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 Stichwort

Diese Idee fügt ein neues Schlüsselwort hinzu super, mit dem Sie die überschriebene Methode auf dieselbe Weise superaufrufen können, mit der Sie die überschriebene Methode aufrufen können :

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar
    old + ' World'
  end
end

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

Das Hauptproblem dabei ist, dass es abwärtskompatibel ist: Wenn Sie eine Methode aufgerufen haben old, können Sie sie nicht mehr aufrufen!

Ersatz

superin einer übergeordneten Methode in einem prepended mixin ist im Wesentlichen das gleiche wie oldin diesem Vorschlag.

redef Stichwort

Ähnlich wie oben, aber anstatt ein neues Schlüsselwort zum Aufrufen der überschriebenen Methode defhinzuzufügen und in Ruhe zu lassen, fügen wir ein neues Schlüsselwort zum Neudefinieren von Methoden hinzu. Dies ist abwärtskompatibel, da die Syntax derzeit ohnehin unzulässig ist:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    old + ' World'
  end
end

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

Anstatt zwei neue Schlüsselwörter hinzuzufügen , könnten wir auch die Bedeutung von superinside neu definieren redef:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    super + ' World'
  end
end

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

Ersatz

redefDas Einfügen einer Methode entspricht dem Überschreiben der Methode in einem prepended-Mixin. superin der überschreibenden Methode verhält sich wie superoder oldin diesem Vorschlag.


@ Jörg W Mittag, ist die Methode Wrapping Approach Thread sicher? Was passiert, wenn zwei gleichzeitige Threads binddieselbe old_methodVariable aufrufen ?
Harish Shetty

1
@KandadaBoggu: Ich versuche herauszufinden, was genau du damit meinst :-) Ich bin mir jedoch ziemlich sicher, dass es nicht weniger threadsicher ist als jede andere Art von Metaprogrammierung in Ruby. Insbesondere wird bei jedem Aufruf von UnboundMethod#bindein neuer, anderer Aufruf zurückgegeben Method, sodass kein Konflikt auftritt, unabhängig davon, ob Sie ihn zweimal hintereinander oder zweimal gleichzeitig aus verschiedenen Threads aufrufen.
Jörg W Mittag

1
Ich suchte nach einer Erklärung für das Patchen wie dieses, seit ich mit Rubin und Schienen angefangen habe. Gute Antwort! Das einzige, was mir fehlte, war ein Hinweis zu class_eval vs. Wiedereröffnung einer Klasse. Hier ist es: stackoverflow.com/a/10304721/188462
Eugene


5
Wo findest du oldund redef? Mein 2.0.0 hat sie nicht. Ah, es ist schwer, die anderen konkurrierenden Ideen, die es nicht in Ruby schafften,
Nakilon

12

Schauen Sie sich Aliasing-Methoden an. Auf diese Weise wird die Methode in einen neuen Namen umbenannt.

Weitere Informationen und einen Ausgangspunkt finden Sie in diesem Artikel über Ersetzungsmethoden (insbesondere im ersten Teil). Die Ruby-API-Dokumente bieten auch ein (weniger ausführliches) Beispiel.


-1

Die Klasse, die überschrieben wird, muss nach der Klasse, die die ursprüngliche Methode enthält, neu geladen werden, damit requiresie in der Datei überschrieben wird.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.