Quando esegui l’applicazione di patch su un metodo, puoi chiamare il metodo sottoposto a override dalla nuova implementazione?

Supponiamo che io stia eseguendo il patching di un metodo in una class, come potrei chiamare il metodo sottoposto a override dal metodo di override? Vale a dire qualcosa di super

Per esempio

 class Foo def bar() "Hello" end end class Foo def bar() super() + " World" end end >> Foo.new.bar == "Hello World" 

EDIT : Sono passati 5 anni da quando ho originariamente scritto questa risposta, e merita un intervento di chirurgia estetica per tenerlo aggiornato.

Puoi vedere l’ultima versione prima della modifica qui .


Non è ansible chiamare il metodo sovrascritto per nome o parola chiave. Questo è uno dei tanti motivi per cui si dovrebbe evitare l’uso di patch per scimmie e preferibilmente preferire l’ereditarietà, poiché ovviamente è ansible chiamare il metodo sottoposto a override .

Evitare patch per scimmie

Eredità

Quindi, se ansible, 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' 

Questo funziona, se controlli la creazione degli oggetti Foo . Basta cambiare ogni posto che crea un Foo per creare invece un ExtendedFoo . Funziona ancora meglio se si utilizza il modello di progettazione dell’iniezione di dipendenza , il modello di progettazione del metodo di fabbrica , il modello di progettazione di una fabbrica astratta o qualcosa del genere, poiché in tal caso, vi è solo un luogo che è necessario modificare.

Delegazione

Se non controlli la creazione degli oggetti Foo , ad esempio perché sono creati da un framework che è al di fuori del tuo controllo (ad esempio ruby-on-rails, ad esempio), puoi utilizzare il modello di design 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' 

Fondamentalmente, al confine del sistema, dove l’object Foo entra nel tuo codice, lo avvolgi in un altro object, e quindi usi quell’object al posto di quello originale ovunque nel tuo codice.

Utilizza il metodo helper Object#DelegateClass dalla libreria delegate nello stdlib.

Patch di scimmia “pulita”

Module#prepend anteporre: Pre-miscelazione

I due metodi sopra richiedono la modifica del sistema per evitare il patch delle scimmie. Questa sezione mostra il metodo preferito e meno invasivo di patch per scimmie, dovrebbe cambiare il sistema non essere un’opzione.

Module#prepend stato aggiunto per supportare più o meno esattamente questo caso d’uso. Module#prepend fa la stessa cosa del Module#include , eccetto che si mescola nel mixin direttamente sotto la class:

 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 ‘su Module#prepend in questa domanda: il modulo Ruby precede rispetto alla derivazione

Mixin Inheritance (rotto)

Ho visto alcune persone provare (e chiedere sul perché non funziona qui su StackOverflow) qualcosa di simile, cioè include un mixin invece di prepend :

 class Foo def bar 'Hello' end end module FooExtensions def bar super + ' World' end end class Foo include FooExtensions end 

Sfortunatamente, questo non funzionerà. È una buona idea, perché usa l’ereditarietà, il che significa che puoi usare super . Tuttavia, il Module#include inserimento del mixin sopra la class nella gerarchia dell’ereditarietà, il che significa che la FooExtensions#bar non verrà mai chiamata (e se fosse chiamata, il super non si riferirebbe alla Foo#bar di Foo#bar ma piuttosto alla Object#bar che non esiste), poiché la Foo#bar verrà sempre trovata per prima.

Avvolgimento del metodo

La grande domanda è: come possiamo aggrapparci al metodo della bar , senza effettivamente mantenere un metodo reale ? La risposta sta, come spesso accade, nella programmazione funzionale. Otteniamo il metodo come object reale e usiamo una chiusura (cioè un blocco) per assicurarci che noi e solo ci aggrappiamo a quell’object:

 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à al di fuori dell’ambito alla fine del corpo della class, ed è imansible accedervi da qualsiasi luogo, anche usando la reflection! E poiché Module#define_method prende un blocco e blocca vicino al loro ambiente lessicale circostante (che è il motivo per cui stiamo usando define_method invece di def qui), esso (e solo esso) avrà ancora accesso a old_bar , anche dopo che è uscito di scopo.

Breve spiegazione:

 old_bar = instance_method(:bar) 

Qui stiamo avvolgendo il metodo bar in un object metodo UnboundMethod e assegnandolo alla variabile locale old_bar . Ciò significa che ora abbiamo un modo per mantenere la bar anche dopo che è stata sovrascritta.

 old_bar.bind(self) 

Questo è un po ‘complicato. Fondamentalmente, in Ruby (e praticamente in tutti i linguaggi OO basati sulla singola spedizione), un metodo è associato a un object ricevitore specifico, chiamato self in Ruby. In altre parole: un metodo sa sempre a quale object è stato chiamato, sa qual è il suo self . Ma, abbiamo afferrato il metodo direttamente da una class, come fa a sapere di cosa self tratta?

Bene, non lo è, ed è per questo che abbiamo bisogno di bind nostro UnboundMethod a un object, che restituirà un object Method che possiamo quindi chiamare. ( UnboundMethod s non può essere chiamato, perché non sanno cosa fare senza conoscere se self .)

E a cosa lo bind ? Lo bind semplicemente a noi stessi, in questo modo si comporterà esattamente come avrebbe fatto la bar originale!

Infine, dobbiamo chiamare il Method restituito da bind . In Ruby 1.9, c’è una bella syntax nuova per quello ( .() ), Ma se si è su 1.8, si può semplicemente usare il metodo call ; è quello che .() viene tradotto in ogni caso.

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

  • Come faccio a fare riferimento a una funzione in Ruby?
  • Il blocco del codice di Ruby è uguale all’espressione lambda di C?

Patch per scimmie “sporche”

catena alias_method

Il problema che stiamo riscontrando con la nostra patch di scimmia è che quando sovrascriviamo il metodo, il metodo è scomparso, quindi non possiamo più chiamarlo. Quindi, facciamo 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 metodo superfluo old_bar . Questo metodo apparirà nella nostra documentazione, verrà mostrato in codice nei nostri IDE, verrà mostrato durante la riflessione. Inoltre, può ancora essere chiamato, ma presumibilmente lo abbiamo modificato, perché non ci piaceva il suo comportamento in primo luogo, quindi potremmo non volere che altre persone lo chiamino.

Nonostante il fatto che questo abbia alcune proprietà indesiderate, purtroppo è diventato popolare attraverso il Module#alias_method_chain .

A parte: perfezionamenti

Nel caso in cui sia necessario il comportamento differente in alcuni punti specifici e non nell’intero sistema, è ansible utilizzare i perfezionamenti per limitare la patch della scimmia a un ambito specifico. Ho intenzione di dimostrarlo qui usando l’esempio di Module#prepend dall’alto:

 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! 

Puoi vedere un esempio più sofisticato di utilizzo dei Rifinimenti in questa domanda: Come abilitare la patch scimmia per un metodo specifico?


Idee abbandonate

Prima che la community di Ruby si fosse stabilita sul Module#prepend , c’erano molte idee diverse che potevano essere viste di tanto in tanto nelle discussioni precedenti. Tutti questi sono riassunti dal Module#prepend .

Combinatori di metodi

Un’idea è stata l’idea di combinatori di metodi di CLOS. Questa è fondamentalmente una versione molto leggera di un sottoinsieme di Programmazione orientata agli aspetti.

Usando la syntax 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 

Sareste in grado di “agganciare” l’esecuzione del metodo della bar .

Non è tuttavia abbastanza chiaro se e come si ottiene l’accesso al valore di ritorno della bar:after all’interno della bar:after . Forse potremmo (ab) usare la parola chiave super ?

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

Sostituzione

Il combinatore precedente equivale a far prepend un mixin con un metodo prevalente che chiama super alla fine del metodo. Allo stesso modo, il dopo combinatore equivale a prepend un mixin con un metodo prevalente che chiama super all’inizio del metodo.

Puoi anche fare cose prima e dopo aver chiamato super , puoi chiamare super volte più volte, ed entrambi recuperare e manipolare il valore di ritorno di super , rendendo l’ 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 in cui super consente di chiamare il metodo sottoposto a override :

 class Foo def bar 'Hello' end end class Foo def bar old + ' World' end end Foo.new.bar # => 'Hello World' 

Il problema principale di questo è che è incompatibile con le versioni precedenti: se hai un metodo chiamato old , non sarai più in grado di chiamarlo!

Sostituzione

super in un metodo prevalente in un prepend è essenzialmente uguale a quello old in questa proposta.

redef parola chiave

Simile a sopra, ma invece di aggiungere una nuova parola chiave per chiamare il metodo sovrascritto e lasciare solo def , aggiungiamo una nuova parola chiave per ridefinire i metodi. Questo è retrocompatibile, dal momento che la syntax al momento è 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 all’interno di redef :

 class Foo def bar 'Hello' end end class Foo redef bar super + ' World' end end Foo.new.bar # => 'Hello World' 

Sostituzione

redef un metodo equivale a sovrascrivere il metodo in un prepend . super nel metodo di sovrascrittura si comporta come super o old in questa proposta.

Dai un’occhiata ai metodi di aliasing, questo è un po ‘come rinominare il metodo con un nuovo nome.

Per ulteriori informazioni e un punto di partenza, dai un’occhiata a questo articolo sui metodi di sostituzione (in particolare la prima parte). I documenti API di Ruby forniscono anche (un esempio meno elaborato).

La class che eseguirà l’override deve essere ricaricata dopo la class che contiene il metodo originale, quindi require nel file che farà overrride.