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 (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 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
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
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.
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.