Quando si applica la patch di scimmia a un metodo di istanza, è possibile chiamare il metodo ignorato dalla nuova implementazione?


444

Supponiamo che io stia correggendo una scimmia con un metodo in una classe, come potrei chiamare il metodo override dal metodo override? Vale a dire qualcosa di similesuper

Per esempio

class Foo
  def bar()
    "Hello"
  end
end 

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

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

La prima classe Foo non dovrebbe essere un'altra e la seconda Foo ereditare da essa?
Draco Ater,

1
no, sto rattoppando la scimmia. Speravo che ci fosse qualcosa di simile a super () che potrei usare per chiamare il metodo originale
James Hollingworth il

1
Ciò è necessario quando non controlli la creazione Foo e l'utilizzo di Foo::bar. Quindi devi scimmia patch il metodo.
Halil Özgür

Risposte:


1166

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 Foooggetti. Basta cambiare ogni luogo che crea un Fooper 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 Foooggetti, ad esempio perché sono creati da un framework al di fuori del tuo controllo (comead 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' Foooggetto 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#DelegateClassmetodo helper dalla delegatelibreria nello stdlib.

Patch di scimmia "pulito"

Module#prepend: Mixin Prepending

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#prependfa 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#prependquesta 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è includeinging un mixin invece di prepending:

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#includeinserisce il mixin sopra la classe nella gerarchia di ereditarietà, il che significa che FooExtensions#barnon sarà mai chiamato (e se fosse chiamato, il supernon sarebbe in realtà fare riferimento a Foo#barquanto piuttosto a Object#barche non esiste), dal momento che Foo#barsarà sempre trovato prima.

Metodo di avvolgimento

La grande domanda è: come possiamo aggrapparci al barmetodo, 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_methodrichiede un blocco e blocchi vicini al loro ambiente lessicale circostante ( motivo per cui stiamo usando define_methodinvece di defqui), 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 barmetodo in un UnboundMethodoggetto metodo e assegnandolo alla variabile locale old_bar. Ciò significa che ora abbiamo un modo per tener duro baranche 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 selfin Ruby. In altre parole: un metodo sa sempre a quale oggetto è stato chiamato, sa di cosa si selftratta. Ma abbiamo preso il metodo direttamente da una classe, come fa a sapere di cosa si selftratta?

Beh, non è così, ed è per questo che dobbiamo bindil nostro UnboundMethoda un oggetto prima, che restituirà un Methodoggetto che si può quindi chiamare. ( UnboundMethodNon si possono chiamare, perché non sanno cosa fare senza conoscere il loro self.)

E cosa facciamo bind? Semplicemente bindper noi stessi, in questo modo si comporterà esattamente come baravrebbe fatto l'originale !

Infine, dobbiamo chiamare il da Methodcui viene restituito bind. In Ruby 1.9, c'è una nuova sintassi elegante per that ( .()), ma se sei su 1.8, puoi semplicemente usare il callmetodo; questo è ciò che .()viene tradotto comunque.

Ecco un paio di altre domande, in cui alcuni di questi concetti sono spiegati:

Patch di scimmie “sporche”

alias_method catena

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

A parte: i perfezionamenti

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#prependesempio 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 barmetodo.

Tuttavia non è del tutto chiaro se e come si accede al barvalore restituito all'interno bar:after. Forse potremmo (ab) usare la superparola chiave?

class Foo
  def bar
    'Hello'
  end
end 

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

Sostituzione

Il combinatore precedente equivale a prependingingare un mixin con un metodo prioritario che chiama superalla fine del metodo. Analogamente, il combinatore dopo equivale a prepending un mixin con un metodo override che le chiamate superal molto inizio del metodo.

Puoi anche fare cose prima e dopo la chiamata super, puoi chiamare superpiù volte e sia recuperare che manipolare superil valore di ritorno, rendendolo prependpiù 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 superconsente 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

superin un metodo prevalente in un prependmixin ed è essenzialmente lo stesso olddi questa proposta.

redef parola chiave

Simile al precedente, ma invece di aggiungere una nuova parola chiave per chiamare il metodo sovrascritto e lasciar defperdere, 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 superinside redef:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    super + ' World'
  end
end

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

Sostituzione

redefining un metodo equivale a sovrascrivere il metodo in un prependmixin ed. supernel metodo prevalente si comporta come supero oldin questa proposta.


@ Jörg W Mittag, il metodo di avvolgimento del thread di approccio è sicuro? Cosa succede quando due thread simultanei chiamano bindsulla stessa old_methodvariabile?
Harish Shetty,

1
@KandadaBoggu: Sto cercando di capire cosa intendi esattamente con questo :-) Tuttavia, sono abbastanza sicuro che non sia meno sicuro del thread rispetto a qualsiasi altro tipo di metaprogrammazione in Ruby. In particolare, ogni chiamata a UnboundMethod#bindrestituirà un nuovo, diverso Method, quindi non vedo sorgere alcun conflitto, indipendentemente dal fatto che tu lo chiami due volte di seguito o due volte contemporaneamente da thread diversi.
Jörg W Mittag,

1
Stavo cercando una spiegazione su come rattoppare da quando ho iniziato su rubini e rotaie. Bella risposta! L'unica cosa che mancava per me era una nota su class_eval contro la riapertura di una classe. Eccolo: stackoverflow.com/a/10304721/188462
Eugene l'


5
Dove trovi olde redef? Il mio 2.0.0 non li ha. Ah, è difficile non perdere le altre idee concorrenti che non sono entrate in Ruby:
Nakilon,


-1

La classe che eseguirà l'override deve essere ricaricata dopo la classe che contiene il metodo originale, quindi requirenel file che eseguirà l'override.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.