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 Foo
Objekte steuern . Ändern Sie einfach jeden Ort, der ein erstellt, Foo
um 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 Foo
Objekte nicht steuern , z. B. weil sie von einem Framework erstellt werden, das außerhalb Ihrer Kontrolle liegt (zRuby-on-RailsZum 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 Foo
wickeln 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#DelegateClass
aus der delegate
Bibliothek in der stdlib.
"Clean" Monkey Patching
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#prepend
wurde hinzugefügt, um mehr oder weniger genau diesen Anwendungsfall zu unterstützen. Module#prepend
macht 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#prepend
in 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 include
ein Mixin anstatt prepend
es 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#include
fügt die mixin über die Klasse in der Vererbungshierarchie, die Mittel , die FooExtensions#bar
nie aufgerufen werden (und wenn es wurden genannt, die super
nicht tatsächlich beziehen sich auf , Foo#bar
sondern auf Object#bar
die es nicht gibt), da Foo#bar
immer zuerst gefunden werden.
Methodenverpackung
Die große Frage ist: Wie können wir an der bar
Methode 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_bar
es 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_method
ein Block benötigt wird und Blöcke über der umgebenden lexikalischen Umgebung geschlossen werden ( weshalb wir define_method
statt def
hier 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 bar
Methode in ein UnboundMethod
Methodenobjekt und weisen es der lokalen Variablen zu old_bar
. Das heißt, wir haben jetzt die Möglichkeit, bar
auch 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 self
in Ruby aufgerufen wird . Mit anderen Worten: Eine Methode weiß immer, auf welchem Objekt sie aufgerufen wurde, sie weiß, was es self
ist. Aber wir haben die Methode direkt aus einer Klasse geholt. Woher weiß sie, was sie self
ist?
Nun, es funktioniert nicht, weshalb wir brauchen bind
unsere UnboundMethod
auf ein Objekt zuerst, das ein zurückkehren Method
Objekt , das wir dann anrufen. ( UnboundMethod
s können nicht angerufen werden, weil sie nicht wissen, was sie tun sollen, ohne ihre zu kennen self
.)
Und wozu machen wir bind
das? Wir haben es einfach bind
für uns, so wird es sich genau so verhalten wie das Original bar
!
Zuletzt müssen wir das zurückrufen, von dem Method
zurü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 call
Methode verwenden. das .()
wird sowieso übersetzt.
Hier sind einige andere Fragen, in denen einige dieser Konzepte erläutert werden:
"Dirty" Monkey Patching
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_bar
Methode 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
.
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#prepend
obigen 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 bar
Methode "einhaken" .
Es ist jedoch nicht ganz klar, ob und wie Sie Zugriff auf bar
den Rückgabewert von erhalten bar:after
. Vielleicht könnten wir das super
Schlüsselwort (ab) verwenden ?
class Foo
def bar
'Hello'
end
end
class Foo
def bar:after
super + ' World'
end
end
Ersatz
Der Vorher-Kombinator entspricht prepend
einem Mixin mit einer überschreibenden Methode, super
die ganz am Ende der Methode aufgerufen wird. Ebenso entspricht der After-Combinator prepend
einem Mixin mit einer überschreibenden Methode, super
die ganz am Anfang der Methode aufgerufen wird.
Sie können auch Dinge vor und nach dem Aufruf erledigen super
, Sie können super
mehrmals aufrufen und super
den Rückgabewert sowohl abrufen als auch bearbeiten , was prepend
leistungsfä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 super
aufrufen 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
super
in einer übergeordneten Methode in einem prepend
ed mixin ist im Wesentlichen das gleiche wie old
in diesem Vorschlag.
redef
Stichwort
Ähnlich wie oben, aber anstatt ein neues Schlüsselwort zum Aufrufen der überschriebenen Methode def
hinzuzufü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 super
inside neu definieren redef
:
class Foo
def bar
'Hello'
end
end
class Foo
redef bar
super + ' World'
end
end
Foo.new.bar # => 'Hello World'
Ersatz
redef
Das Einfügen einer Methode entspricht dem Überschreiben der Methode in einem prepend
ed-Mixin. super
in der überschreibenden Methode verhält sich wie super
oder old
in diesem Vorschlag.