Bir örnek yöntemine yama eklerken, yeni uygulamadan geçersiz kılınan yöntemi çağırabilirsiniz?


444

Diyelim ki bir sınıftaki bir yöntemi yamalıyorum, geçersiz kılma yönteminden geçersiz kılınan yöntemi nasıl çağırabilirim? Yani birazsuper

Örneğin

class Foo
  def bar()
    "Hello"
  end
end 

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

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

İlk Foo sınıfı başka biri olmamalı ve ikinci Foo sınıftan miras almamalıdır?
Draco Ater

1
hayır, ben maymun yamasıyım. Orijinal yöntemi çağırmak için kullanabileceğim süper () gibi bir şey olacağını umuyordum
James Hollingworth

1
Bu öğenin oluşturulmasını Foo ve kullanımını denetlemediğinizde gereklidir Foo::bar. Yani yöntemi yama maymun gerekir .
Halil Özgür

Yanıtlar:


1166

EDIT : Aslında bu cevabı yazdığım 9 yıl oldu ve güncel tutmak için bazı kozmetik cerrahiyi hak ediyor.

Düzenlemeden önceki son sürümü burada görebilirsiniz .


Üzerine yazılan yöntemi ada veya anahtar sözcüğe göre çağıramazsınız . Açıkçası beri bu maymun yama kaçınılmalıdır ve miras yerine tercih edilmesi birçok nedenden biri olabilir diyoruz geçersiz kılınan yöntem.

Maymun Yamadan Kaçının

miras

Yani, eğer mümkünse, böyle bir şeyi tercih etmelisiniz:

class Foo
  def bar
    'Hello'
  end
end 

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

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

FooNesnelerin oluşturulmasını kontrol ediyorsanız, bu işe yarar . Bunun Fooyerine , bir oluşturmak için her yeri değiştirin ExtendedFoo. Bağımlılık Enjeksiyon Tasarım Deseni , Fabrika Metodu Tasarım Deseni , Soyut Fabrika Tasarım Deseni veya bu çizgiler boyunca bir şey kullanırsanız, bu daha da iyi çalışır , çünkü bu durumda yalnızca değiştirmeniz gereken yer vardır.

delegasyon

Eğer varsa yok oluşturulmasını kontrol Fooonlar sizin kontrolünüz dışında olan bir çerçeve tarafından oluşturulur, çünkü böyle (örneğin nesnelerin,örneğin), Sarıcı Tasarım Deseni'ni kullanabilirsiniz :

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'

Temel olarak, Foonesnenin kodunuza girdiği sistemin sınırında, onu başka bir nesneye sararsınız ve daha sonra , kodunuzdaki başka her yerde orijinal yerine bu nesneyi kullanırsınız.

Bu , stdlib'deki kütüphaneden Object#DelegateClassyardımcı yöntemi kullanır delegate.

“Temiz” Maymun Yamalama

Module#prepend: Mixin Hazırlanıyor

Yukarıdaki iki yöntem, maymun yamasını önlemek için sistemin değiştirilmesini gerektirir. Bu bölüm, sistemi değiştirmenin bir seçenek olmaması gerektiğinde, tercih edilen ve en az invaziv maymun yaması yöntemini göstermektedir.

Module#prependbu kullanım örneğini az çok desteklemek için eklendi. sınıfın hemen altındaki karışımda karışması dışında Module#prependaynı şeyi yapar :Module#include

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'

Not: Ben de Module#prependbu soru hakkında biraz yazdım : Ruby modülü türetmek vs türev

Mixin Kalıtım (kırık)

Bazı insanlar denemek gördüm (ve neden burada StackOverflow üzerinde çalışmadığı hakkında sormak) böyle bir şey, yani includeing yerine bir mixin prepending:

class Foo
  def bar
    'Hello'
  end
end 

module FooExtensions
  def bar
    super + ' World'
  end
end

class Foo
  include FooExtensions
end

Ne yazık ki, bu işe yaramaz. Bu iyi bir fikir, çünkü miras kullanıyor, yani kullanabileceğiniz anlamına geliyor super. Ancak, Module#includemixin ekler yukarıdaki araçlar kalıtım hiyerarşisinde sınıfı FooExtensions#bardenilen asla (ve eğer edildi denilen, superaslında atıfta olmaz Foo#barziyade üzere Object#barberi var olmayan) Foo#barher zaman ilk bulunacaktır.

Yöntem Sarma

Büyük soru şudur: Gerçek bir yöntemibar tutmadan yönteme nasıl dayanabiliriz ? Cevap işlevsel programlamada sıklıkla olduğu gibi yatıyor. Yöntemin gerçek bir nesne olarak tutulduğunu ve yalnızca ve yalnızca o nesneye bağlı kaldığımızdan emin olmak için bir kapatma (yani bir blok) kullanıyoruz :

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'

Bu çok temiz: old_barsadece yerel bir değişken olduğu için sınıf gövdesinin sonunda kapsam dışına çıkacak ve yansıma kullanarak bile her yerden erişmek imkansız ! Ve Module#define_methodbir blok aldığından ve çevreleyen sözcüksel ortamını kapattığından (bu yüzden burada kullanmak define_methodyerine kullanıyoruz def), o (ve sadece o) old_barkapsam dışına çıktıktan sonra bile hala erişime sahip olacaktır .

Kısa açıklama:

old_bar = instance_method(:bar)

Burada baryöntemi bir UnboundMethodyöntem nesnesine sararak yerel değişkene atarız old_bar. Bu, barüzerine yazıldıktan sonra bile tutunmamız gereken bir yol var .

old_bar.bind(self)

Bu biraz zor. Temel olarak, Ruby'de (ve hemen hemen tüm tek dağıtım tabanlı OO dillerinde), yöntem selfRuby'de adlandırılan belirli bir alıcı nesnesine bağlanır . Başka bir deyişle: bir yöntem her zaman hangi nesneye çağrıldığını bilir, onun ne olduğunu bilir self. Ancak, yöntemi doğrudan bir sınıftan aldık, bunun ne olduğunu nasıl biliyor self?

Eh, biz gereken, bu yüzden de değil bindOur UnboundMethodbir dönecektir ilk bir nesne ile Methodo zaman diyoruz ki nesneyi. ( UnboundMethodçağrılamazlar, çünkü bilmeden ne yapacaklarını bilmiyorlar self.)

Ve ne yapacağız bind? Biz sadece bindkendimize, bu şekilde davranacaktır aynen orijinal gibi barolurdu!

Son olarak, Methodgeri gönderileni aramalıyız bind. Ruby 1.9'da, bunun için bazı şık yeni sözdizimi vardır ( .()), ancak 1.8 üzerindeyseniz, callyöntemi kullanabilirsiniz ; Ne en o .()zaten için tercüme.

İşte bu kavramlardan bazılarının açıklandığı birkaç soru daha:

“Kirli” Maymun Yamaları

alias_method Zincir

Maymun yamasıyla ilgili yaşadığımız sorun, yöntemin üzerine yazdığımızda yöntemin gitmiş olmasıdır, bu yüzden artık arayamayız. Şimdi yedek bir kopya yapalım!

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'

Buradaki sorun, artık ad alanını gereksiz bir old_baryöntemle kirletmiş olmamız . Bu yöntem dokümanlarımızda görünecek, IDE'lerimizde kod tamamlandığında görünecek, yansıma sırasında görünecektir. Ayrıca, hala çağrılabilir, ancak muhtemelen maymun yamalı, çünkü ilk etapta davranışını beğenmedik, bu yüzden diğer insanların aramasını istemeyebiliriz.

Bunun istenmeyen bazı özelliklere sahip olmasına rağmen, ne yazık ki AciveSupport's aracılığıyla popüler hale geldi Module#alias_method_chain.

Bir yana: Ayrıntılandırmalar

Sistemin tamamında değil, yalnızca birkaç belirli yerde farklı davranışa ihtiyacınız varsa, maymun yamasını belirli bir kapsamla sınırlamak için İyileştirmeler'i kullanabilirsiniz. Ben Module#prependyukarıdaki örneği kullanarak burada göstermek için gidiyorum :

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!

Bu soruda Ayrıntılandırmaları kullanmanın daha karmaşık bir örneğini görebilirsiniz: Maymun düzeltme eki belirli bir yöntem için nasıl etkinleştirilir?


Terk edilmiş fikirler

Ruby topluluğu yerleşmeden önce, Module#prependetrafta yüzen, bazen daha eski tartışmalarda atıfta bulunabileceğiniz birçok farklı fikir vardı. Bunların hepsi tarafından toplanır Module#prepend.

Yöntem Kombinatörleri

Bir fikir, CLOS'tan yöntem birleştiriciler fikriydi. Bu temel olarak Aspect-Oriented Programming'in bir alt kümesinin çok hafif bir sürümüdür.

Gibi sözdizimi kullanma

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

baryöntemin yürütülmesine “bağlanabilirsiniz” .

Bununla birlikte, bariçindeki dönüş değerine erişip erişmediğinizi ve nasıl erişeceğinizi açık değildir bar:after. Belki (ab) superanahtar kelimesini kullanabiliriz ?

class Foo
  def bar
    'Hello'
  end
end 

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

Değiştirme

Önceki birleştirici, prependyöntemin superen sonunda çağıran geçersiz kılma yöntemine sahip bir mixin oluşturmakla eşdeğerdir . Benzer şekilde, sonraki birleştirici, prependyöntemin superen başında çağıran geçersiz kılma yöntemine sahip bir mixin oluşturmakla eşdeğerdir .

Ayrıca , aramadan önce ve sonra bir şeyler superyapabilir, superbirden çok kez arayabilir ve hem geri superdönüş değerini alıp hem de manipüle ederek prependyöntem birleştiricilerinden daha güçlü hale getirebilirsiniz .

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

ve

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 anahtar kelime

Bu fikir super, üzerine yazılan yöntemi aynı şekilde geçersiz kılma yöntemini superçağırmanıza izin veren yeni bir anahtar kelime ekler :

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar
    old + ' World'
  end
end

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

Bununla ilgili temel sorun, geriye dönük uyumsuz olmasıdır: çağrılan bir yönteminiz oldvarsa, artık arayamazsınız!

Değiştirme

superprepended kanşımındaki geçersiz kılma yönteminde esasen oldbu öneri ile aynıdır .

redef anahtar kelime

Yukarıdakine benzer, ancak üzerine yazılan yöntemi çağırmak ve defyalnız bırakmak için yeni bir anahtar kelime eklemek yerine, yöntemleri yeniden tanımlamak için yeni bir anahtar kelime ekliyoruz . Şu anda sözdizimi yasadışı olduğu için bu geriye dönük olarak uyumludur:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    old + ' World'
  end
end

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

Bunun yerine eklemenin iki yeni anahtar kelimeler, biz de bir anlam yeniden tanımlamak olabilir superiçeriden redef:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    super + ' World'
  end
end

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

Değiştirme

redefbir yöntemin uygulanması, bir prependedim karışımında yöntemin geçersiz kılınmasına denktir . supergeçersiz kılma yönteminde bu teklif gibi superveya oldbu davranışta bulunur .


@ Jörg W Mittag, yöntem sarma yaklaşımı iş parçacığı güvenli midir? bindAynı old_methoddeğişken üzerinde iki eşzamanlı iş parçacığı çağırdığında ne olur ?
Harish Shetty

1
@KandadaBoggu: Bununla tam olarak ne demek istediğini anlamaya çalışıyorum :-) Ancak, Ruby'de başka herhangi bir metaprogramlamadan daha az güvenli değildir. Özellikle, her çağrı UnboundMethod#bindyeni, farklı bir geri dönecektir Method, bu yüzden, üst üste iki kez veya farklı iş parçacıklarından aynı anda iki kez aradığınızdan bağımsız olarak herhangi bir çatışma görmüyorum.
Jörg W Mittag

1
Yakut ve raylarda başladığımdan beri böyle yamalar hakkında bir açıklama arıyordum. Mükemmel cevap! Benim için eksik olan tek şey class_eval üzerine bir sınıfı yeniden açmakla ilgili bir nottu. İşte: stackoverflow.com/a/10304721/188462
Eugene


5
Nerede bulurum oldve redef? 2.0.0 sürümümde yok. Ah,
Ruby'ye girmeyen

12

Diğer adlandırma yöntemlerine bir göz atın, bu yöntemi yeni bir adla yeniden adlandırmak anlamına gelir.

Daha fazla bilgi ve başlangıç ​​noktası için bu değiştirme yöntemleri makalesine (özellikle ilk bölüme) bir göz atın . Yakut API dosyaları , ayrıca (daha az karmaşık bir) örnek sunmaktadır.


-1

Geçersiz kılmayı yapacak sınıfın, özgün yöntemi içeren requiredersten sonra yeniden yüklenmesi gerekir, böylece geçersiz kılmayı yapacak dosyada.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.