Czy w przypadku małpowania łatania metody instancji można wywołać metodę zastąpioną z nowej implementacji?

444

Powiedzmy, że jestem małpą łatającą metodę w klasie, jak mogę wywołać metodę przesłoniętą z metody przesłonięcia? Tj. Coś trochę jaksuper

Na przykład

class Foo
  def bar()
    "Hello"
  end
end 

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

>> Foo.new.bar == "Hello World"
James Hollingworth
źródło
Czy pierwsza klasa Foo nie powinna być inna, a druga Foo po niej odziedziczyć?
Draco Ater,
1
nie, jestem łataniem małp. Miałem nadzieję, że będzie coś w rodzaju super (), którego mógłbym użyć do wywołania oryginalnej metody
James Hollingworth,
1
Jest to potrzebne, gdy nie kontrolujesz tworzenia Foo i użytkowania Foo::bar. Więc musisz małpować łatanie metody.
Halil Özgür

Odpowiedzi:

1165

EDYCJA : Minęło 9 lat, odkąd pierwotnie napisałem tę odpowiedź i zasługuję na chirurgię plastyczną, aby była aktualna.

Można zobaczyć ostatnią wersję przed edit tutaj .


Nie można wywołać zastąpionej metody według nazwy lub słowa kluczowego. To jeden z wielu powodów, dla których należy unikać łatania małp i zamiast tego preferować dziedziczenie, ponieważ oczywiście można wywołać metodę przesłoniętą .

Unikanie łatania małp

Dziedzictwo

Więc jeśli to w ogóle możliwe, powinieneś preferować coś takiego:

class Foo
  def bar
    'Hello'
  end
end 

class ExtendedFoo < Foo
  def bar
    super + ' World'
  end
end

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

Działa to, jeśli kontrolujesz tworzenie Fooobiektów. Po prostu zmień każde miejsce, które tworzy Foozamiast ExtendedFoo. Działa to jeszcze lepiej, jeśli użyjesz wzoru projektu wtrysku zależności , tym wzór Metoda Factory Design The wzór abstrakcyjny Factory Design lub coś wzdłuż tych linii, ponieważ w tym przypadku nie jest miejsce tylko trzeba zmienić.

Delegacja

Jeśli ty nie kontrolujesz tworzenia Fooobiektów, na przykład ponieważ są one tworzone przez strukturę, która jest poza twoją kontrolą (npna przykład), możesz użyć Wzorca Projektu Opakowania :

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'

Zasadniczo na granicy systemu, w której Fooobiekt wchodzi do twojego kodu, zawijasz go w inny obiekt, a następnie używasz tego obiektu zamiast oryginalnego w dowolnym miejscu w kodzie.

Używa to Object#DelegateClassmetody pomocniczej zdelegate biblioteki w stdlib.

„Czysta” łatka małp

Module#prepend: Mixin Prepending

Dwie powyższe metody wymagają zmiany systemu, aby uniknąć łatania małp. W tej sekcji pokazano preferowaną i najmniej inwazyjną metodę łatania małp. Zmiana systemu nie powinna być opcją.

Module#prependzostał dodany w celu obsługi mniej więcej dokładnie tego przypadku użycia. Module#prependrobi to samo Module#include, z tym wyjątkiem, że miesza się w mixin bezpośrednio poniżej klasy:

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'

Uwaga: napisałem też trochę o Module#prependtym pytaniu: moduł Ruby prepend vs wyprowadzenie

Dziedziczenie Mixin (uszkodzony)

Widziałem, jak niektórzy ludzie próbują (i pytają, dlaczego to nie działa tutaj na StackOverflow) coś takiego, np. includeWkładając mixin zamiast prependgo:

class Foo
  def bar
    'Hello'
  end
end 

module FooExtensions
  def bar
    super + ' World'
  end
end

class Foo
  include FooExtensions
end

Niestety to nie zadziała. To dobry pomysł, ponieważ wykorzystuje dziedziczenie, co oznacza, że ​​możesz używać super. Jednak Module#includewstawia mixin powyżej klasy w hierarchii dziedziczenia, co oznacza, że FooExtensions#barnigdy nie można nazwać (a jeśli były nazywa, supernie odnoszą się do rzeczywistości Foo#bar, lecz do Object#barktórych nie istnieje), ponieważ Foo#barzawsze znajdują się w pierwszej kolejności.

Metoda owijania

Najważniejsze pytanie brzmi: w jaki sposób możemy trzymać się tej barmetody bez faktycznego trzymania się tej metody ? Odpowiedź leży, jak to często bywa, w programowaniu funkcjonalnym. Utrzymujemy metodę jako rzeczywisty obiekt i używamy zamknięcia (tj. Bloku), aby upewnić się, że my i tylko my trzymamy się tego obiektu:

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'

Jest to bardzo czyste: ponieważ old_barjest to tylko zmienna lokalna, na końcu ciała klasy wyjdzie poza zakres i nie można uzyskać do niej dostępu z dowolnego miejsca, nawet przy użyciu odbicia! A ponieważ Module#define_methodbierze blok, a bloki zamykają otaczające ich środowisko leksykalne ( dlatego używamy define_methodzamiast deftutaj), to (i tylko on) nadal będzie miał dostęp old_bar, nawet po tym, jak wyszedł poza zakres.

Krótkie wyjaśnienie:

old_bar = instance_method(:bar)

W tym miejscu zawijamy barmetodę do UnboundMethodobiektu metody i przypisujemy do zmiennej lokalnej old_bar. Oznacza to, że mamy teraz sposób, aby się zatrzymać barnawet po jego zastąpieniu.

old_bar.bind(self)

To trochę trudne. Zasadniczo w języku Ruby (i prawie we wszystkich językach OO opartych na pojedynczej wysyłce) metoda jest powiązana z konkretnym obiektem odbiorczym, zwanym selfw języku Ruby. Innymi słowy: metoda zawsze wie, do którego obiektu została wywołana, wie, co to selfjest. Ale wzięliśmy metodę bezpośrednio z klasy, skąd ona wie, co to selfjest?

Cóż, to nie, dlatego musimy bindOur UnboundMethoddo pierwszego obiektu, który zwróci Methodobiekt, który możemy wywołać. ( UnboundMethodnie można się nazywać, ponieważ nie wiedzą, co robić, nie znając ich self).

A do czego to robimy bind? Po prostu bindto dla siebie, w ten sposób zachowa się dokładnie tak , jak oryginał bar!

Na koniec musimy zadzwonić pod ten, Methodktóry jest zwracany bind. W Ruby 1.9 dostępna jest nowa, świetna składnia ( .()), ale jeśli korzystasz z wersji 1.8, możesz po prostu użyćcall metody; i tak .()zostaje przetłumaczone.

Oto kilka innych pytań, w których wyjaśniono niektóre z tych pojęć:

Łata „Brudna” Małpa

alias_method łańcuch

Problem z łataniem małp polega na tym, że gdy nadpisujemy metodę, metoda znika, więc nie możemy jej już nazwać. Zróbmy więc kopię zapasową!

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'

Problem polega na tym, że przestaliśmy zanieczyszczać przestrzeń nazw zbędnym old_bar metodą. Ta metoda pojawi się w naszej dokumentacji, pokaże się po zakończeniu kodu w naszych IDE, pojawi się podczas refleksji. Ponadto nadal można go nazwać, ale prawdopodobnie załataliśmy go małpą, ponieważ nie podobało nam się jego zachowanie, więc możemy nie chcieć, aby inni go nazywali.

Pomimo tego, że ma to pewne niepożądane właściwości, niestety zostało spopularyzowane przez AciveSupport Module#alias_method_chain.

Na bok: udoskonalenia

Jeśli potrzebujesz innego zachowania tylko w kilku określonych miejscach, a nie w całym systemie, możesz użyć Udoskonaleń, aby ograniczyć łatkę małpy do określonego zakresu. Pokażę to tutaj na Module#prependpowyższym przykładzie:

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!

Bardziej wyrafinowany przykład użycia udoskonaleń można znaleźć w tym pytaniu: Jak włączyć łatkę małpy dla określonej metody?


Porzucone pomysły

Zanim społeczność Ruby się osiedliła Module#prepend, krążyło wokół niej wiele różnych pomysłów, o których od czasu do czasu można się odwoływać w starszych dyskusjach. Wszystkie są uwzględnione przez Module#prepend.

Kombinatory metod

Jednym z pomysłów był pomysł metody łączenia z CLOS. Jest to w zasadzie bardzo lekka wersja podzbioru programowania zorientowanego na aspekty.

Używając podobnej składni

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

będziesz w stanie „zaczepić” się o wykonanie barmetody.

Nie jest jednak do końca jasne, czy i jak uzyskać dostęp do barzwracanej wartości bar:after. Może moglibyśmy (ab) użyć supersłowa kluczowego?

class Foo
  def bar
    'Hello'
  end
end 

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

Zastąpienie

Kombinator przed równoważny jest równoważeniem prependmiksu z metodą nadpisującą, która wywołuje superna samym końcu metody. Podobnie, po kombiatororze jest równoważne z prependwprowadzeniem mixinu za pomocą metody nadpisującej, która wywołuje superna samym początku metody.

Możesz także robić rzeczy przed i po wywołaniu super, możesz wywoływać superwiele razy, a także zarówno pobierać, jak i manipulować superwartością zwracaną, czyniąc to prependsilniejszym niż kombinatory metod.

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

i

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 słowo kluczowe

Ten pomysł dodaje nowe słowo kluczowe podobne do super, które pozwala wywołać zastąpioną metodę w taki sam sposób super, jak wywołać zastąpioną metodę:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar
    old + ' World'
  end
end

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

Główny problem polega na tym, że jest on niezgodny wstecz: jeśli masz wywoływaną metodę old, nie będziesz już w stanie jej wywołać!

Zastąpienie

superw nadrzędnej metodzie w prepended mixin jest zasadniczo taki sam jak oldw tej propozycji.

redef słowo kluczowe

Podobnie jak powyżej, ale zamiast dodawać nowe słowo kluczowe w celu wywołania zastąpionej metody i pozostawienia go w defspokoju, dodajemy nowe słowo kluczowe w celu przedefiniowania metod. Jest to kompatybilne wstecz, ponieważ i tak obecnie składnia jest nielegalna:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    old + ' World'
  end
end

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

Zamiast dodawać dwa nowe słowa kluczowe, moglibyśmy również przedefiniować znaczenie superwewnątrz redef:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    super + ' World'
  end
end

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

Zastąpienie

redefwprowadzenie metody jest równoważne z przesłonięciem metody w prependedytowanym mixinie. superw metodzie zastępowania zachowuje się jak superlub oldw tej propozycji.

Jörg W Mittag
źródło
@ Jörg W Mittag, czy wątek podejścia do owijania metod jest bezpieczny? Co dzieje się, gdy dwa współbieżne wątki wywołują bindtę samą old_methodzmienną?
Harish Shetty
1
@KandadaBoggu: Próbuję dowiedzieć się, co dokładnie masz na myśli :-) Jednak jestem pewien, że jest nie mniej bezpieczny dla wątków niż jakikolwiek inny metaprogramowanie w Ruby. W szczególności każde wezwanie do UnboundMethod#bindzwróci nowy, inny Method, więc nie widzę żadnego konfliktu, niezależnie od tego, czy wywołujesz go dwa razy z rzędu, czy dwa razy w tym samym czasie z różnych wątków.
Jörg W Mittag
1
Szukałem takiego wytłumaczenia, odkąd zacząłem używać rubinu i szyn. Świetna odpowiedź! Jedyne, czego mi brakowało, to notatka o class_eval vs. ponownym otwarciu klasy. Oto on: stackoverflow.com/a/10304721/188462
Eugene
5
Gdzie można znaleźć oldi redef? Mój 2.0.0 ich nie ma. Ach, trudno nie przeoczyć Inne konkurencyjne pomysły, które nie znalazły się w Ruby to:
Nakilon
12

Spójrz na metody aliasingu, jest to rodzaj zmiany nazwy metody na nową nazwę.

Aby uzyskać więcej informacji i punkt wyjścia, zapoznaj się z tym artykułem na temat metod zastępowania (zwłaszcza w pierwszej części). Dokumenty interfejsu API Ruby również stanowią (mniej skomplikowany) przykład.

Veger
źródło
-1

Klasa, która spowoduje zastąpienie, musi zostać ponownie załadowana po klasie zawierającej oryginalną metodę, więc requirew pliku, który spowoduje zastąpienie.

rplaurindo
źródło