EDIT : Sono passati 9 anni da quando ho scritto inizialmente questa risposta, e merita un intervento di chirurgia estetica per tenerlo aggiornato.
Puoi vedere l'ultima versione prima della modifica qui .
Non è possibile chiamare il metodo sovrascritto per nome o parola chiave. Questo è uno dei tanti motivi per cui è necessario evitare il patching delle scimmie e preferire l'ereditarietà, poiché ovviamente è possibile chiamare il metodo ignorato .
Evitare il patching delle scimmie
Eredità
Quindi, se possibile, dovresti preferire qualcosa del genere:
class Foo
def bar
'Hello'
end
end
class ExtendedFoo < Foo
def bar
super + ' World'
end
end
ExtendedFoo.new.bar # => 'Hello World'
Funziona se controlli la creazione degli Foo
oggetti. Basta cambiare ogni luogo che crea un Foo
per creare invece un ExtendedFoo
. Funziona ancora meglio se usi il modello di progettazione dell'iniezione di dipendenza , il modello di progettazione del metodo di fabbrica , il modello di progettazione di fabbrica astratta o qualcosa del genere, perché in quel caso c'è solo un posto che devi cambiare.
Delegazione
Se non controlli la creazione degli Foo
oggetti, ad esempio perché sono creati da un framework al di fuori del tuo controllo (comeruby-on-railsad esempio), quindi è possibile utilizzare il modello di progettazione 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'
In sostanza, al confine del sistema, in cui l' Foo
oggetto entra nel codice, si avvolge in un altro oggetto, e quindi utilizzare quella oggetto invece di quello originale ovunque nel codice.
Questo utilizza il Object#DelegateClass
metodo helper dalla delegate
libreria nello stdlib.
Patch di scimmia "pulito"
I due metodi sopra richiedono la modifica del sistema per evitare l'applicazione di patch scimmia. Questa sezione mostra il metodo preferito e meno invasivo di rattoppare le scimmie, qualora la modifica del sistema non fosse un'opzione.
Module#prepend
è stato aggiunto per supportare più o meno esattamente questo caso d'uso. Module#prepend
fa la stessa cosa Module#include
, tranne che si mescola nel mixin direttamente sotto 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'
Nota: ho anche scritto un po 'di Module#prepend
questa domanda: il modulo Ruby antepone vs derivazione
Eredità mixin (rotta)
Ho visto alcune persone provare (e chiedere perché non funziona qui su StackOverflow) qualcosa del genere, cioè include
inging un mixin invece di prepend
ing:
class Foo
def bar
'Hello'
end
end
module FooExtensions
def bar
super + ' World'
end
end
class Foo
include FooExtensions
end
Sfortunatamente, non funzionerà. È una buona idea, perché usa l'eredità, il che significa che puoi usarla super
. Tuttavia, Module#include
inserisce il mixin sopra la classe nella gerarchia di ereditarietà, il che significa che FooExtensions#bar
non sarà mai chiamato (e se fosse chiamato, il super
non sarebbe in realtà fare riferimento a Foo#bar
quanto piuttosto a Object#bar
che non esiste), dal momento che Foo#bar
sarà sempre trovato prima.
Metodo di avvolgimento
La grande domanda è: come possiamo aggrapparci al bar
metodo, senza tenere effettivamente un metodo reale ? La risposta sta, come spesso accade, nella programmazione funzionale. Otteniamo una sospensione del metodo come un oggetto reale e usiamo una chiusura (cioè un blocco) per assicurarci che noi e solo noi ci aggrappiamo a quell'oggetto:
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'
Questo è molto pulito: poiché old_bar
è solo una variabile locale, andrà fuori portata alla fine del corpo della classe ed è impossibile accedervi da qualsiasi luogo, anche usando la riflessione! E poiché Module#define_method
richiede un blocco e blocchi vicini al loro ambiente lessicale circostante ( motivo per cui stiamo usando define_method
invece di def
qui), esso (e solo esso) avrà ancora accesso old_bar
, anche dopo che è uscito dal campo di applicazione.
Breve spiegazione:
old_bar = instance_method(:bar)
Qui stiamo avvolgendo il bar
metodo in un UnboundMethod
oggetto metodo e assegnandolo alla variabile locale old_bar
. Ciò significa che ora abbiamo un modo per tener duro bar
anche dopo che è stato sovrascritto.
old_bar.bind(self)
Questo è un po 'complicato. Fondamentalmente, in Ruby (e praticamente in tutti i linguaggi OO basati su singolo invio), un metodo è associato a un oggetto ricevitore specifico, chiamato self
in Ruby. In altre parole: un metodo sa sempre a quale oggetto è stato chiamato, sa di cosa si self
tratta. Ma abbiamo preso il metodo direttamente da una classe, come fa a sapere di cosa si self
tratta?
Beh, non è così, ed è per questo che dobbiamo bind
il nostro UnboundMethod
a un oggetto prima, che restituirà un Method
oggetto che si può quindi chiamare. ( UnboundMethod
Non si possono chiamare, perché non sanno cosa fare senza conoscere il loro self
.)
E cosa facciamo bind
? Semplicemente bind
per noi stessi, in questo modo si comporterà esattamente come bar
avrebbe fatto l'originale !
Infine, dobbiamo chiamare il da Method
cui viene restituito bind
. In Ruby 1.9, c'è una nuova sintassi elegante per that ( .()
), ma se sei su 1.8, puoi semplicemente usare il call
metodo; questo è ciò che .()
viene tradotto comunque.
Ecco un paio di altre domande, in cui alcuni di questi concetti sono spiegati:
Patch di scimmie “sporche”
Il problema che stiamo riscontrando con il patching delle scimmie è che quando sovrascriviamo il metodo, il metodo scompare, quindi non possiamo più chiamarlo. Quindi, facciamo solo una copia di backup!
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'
Il problema è che ora abbiamo inquinato lo spazio dei nomi con un old_bar
metodo superfluo . Questo metodo verrà mostrato nella nostra documentazione, verrà mostrato nel completamento del codice nei nostri IDE, verrà mostrato durante la riflessione. Inoltre, può ancora essere chiamato, ma presumibilmente l'abbiamo corretto, perché in primo luogo non ci è piaciuto il suo comportamento, quindi potremmo non volere che altre persone lo chiamino.
Nonostante abbia alcune proprietà indesiderabili, sfortunatamente è diventato popolare attraverso AciveSupport's Module#alias_method_chain
.
Nel caso in cui sia necessario il diverso comportamento solo in alcuni punti specifici e non nell'intero sistema, è possibile utilizzare i perfezionamenti per limitare la patch della scimmia a un ambito specifico. Lo dimostrerò qui usando l' Module#prepend
esempio sopra:
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 questa domanda puoi vedere un esempio più sofisticato dell'uso dei perfezionamenti: Come abilitare la patch scimmia per un metodo specifico?
Idee abbandonate
Prima che la comunità di Ruby si stabilisse Module#prepend
, c'erano diverse idee fluttuanti intorno che potresti occasionalmente vedere referenziate in discussioni precedenti. Tutti questi sono inclusi in Module#prepend
.
Combinatori di metodi
Un'idea era l'idea dei combinatori di metodi di CLOS. Questa è fondamentalmente una versione molto leggera di un sottoinsieme della programmazione orientata agli aspetti.
Utilizzando la sintassi come
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
si sarebbe in grado di "agganciarsi" all'esecuzione del bar
metodo.
Tuttavia non è del tutto chiaro se e come si accede al bar
valore restituito all'interno bar:after
. Forse potremmo (ab) usare la super
parola chiave?
class Foo
def bar
'Hello'
end
end
class Foo
def bar:after
super + ' World'
end
end
Sostituzione
Il combinatore precedente equivale a prepend
ingingare un mixin con un metodo prioritario che chiama super
alla fine del metodo. Analogamente, il combinatore dopo equivale a prepend
ing un mixin con un metodo override che le chiamate super
al molto inizio del metodo.
Puoi anche fare cose prima e dopo la chiamata super
, puoi chiamare super
più volte e sia recuperare che manipolare super
il valore di ritorno, rendendolo prepend
più potente dei combinatori di metodi.
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
e
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
parola chiave
Questa idea aggiunge una nuova parola chiave simile a super
, che consente di chiamare il metodo sovrascritto allo stesso modo super
consente di chiamare il metodo sostituito :
class Foo
def bar
'Hello'
end
end
class Foo
def bar
old + ' World'
end
end
Foo.new.bar # => 'Hello World'
Il problema principale è che è incompatibile con le versioni precedenti: se si dispone di un metodo chiamato old
, non sarà più possibile chiamarlo!
Sostituzione
super
in un metodo prevalente in un prepend
mixin ed è essenzialmente lo stesso old
di questa proposta.
redef
parola chiave
Simile al precedente, ma invece di aggiungere una nuova parola chiave per chiamare il metodo sovrascritto e lasciar def
perdere, aggiungiamo una nuova parola chiave per ridefinire i metodi. Questo è retrocompatibile, poiché la sintassi attualmente è illegale comunque:
class Foo
def bar
'Hello'
end
end
class Foo
redef bar
old + ' World'
end
end
Foo.new.bar # => 'Hello World'
Invece di aggiungere due nuove parole chiave, potremmo anche ridefinire il significato di super
inside redef
:
class Foo
def bar
'Hello'
end
end
class Foo
redef bar
super + ' World'
end
end
Foo.new.bar # => 'Hello World'
Sostituzione
redef
ining un metodo equivale a sovrascrivere il metodo in un prepend
mixin ed. super
nel metodo prevalente si comporta come super
o old
in questa proposta.