Ketika monyet menambal metode contoh, dapatkah Anda memanggil metode yang diganti dari implementasi yang baru?

444

Katakanlah saya monyet menambal metode di kelas, bagaimana saya bisa memanggil metode yang ditimpa dari metode yang menimpa? Yaitu Sesuatu yang agak sepertisuper

Misalnya

class Foo
  def bar()
    "Hello"
  end
end 

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

>> Foo.new.bar == "Hello World"
James Hollingworth
sumber
Bukankah seharusnya kelas Foo pertama adalah kelas lain dan Foo kedua mewarisinya?
Draco Ater
1
tidak, saya menambal monyet. Saya berharap akan ada sesuatu seperti super () yang bisa saya gunakan untuk memanggil metode asli
James Hollingworth
1
Ini diperlukan saat Anda tidak mengontrol pembuatan Foo dan penggunaan Foo::bar. Jadi, Anda harus menambal metode ini.
Halil Özgür

Jawaban:

1166

EDIT : Sudah 9 tahun sejak saya awalnya menulis jawaban ini, dan perlu beberapa operasi kosmetik agar tetap mutakhir.

Anda dapat melihat versi terakhir sebelum edit di sini .


Anda tidak dapat memanggil metode yang ditimpa dengan nama atau kata kunci. Itulah salah satu dari banyak alasan mengapa penambalan monyet harus dihindari dan pewarisan lebih disukai, karena jelas Anda dapat memanggil metode yang diganti .

Menghindari Penambalan Monyet

Warisan

Jadi, jika memungkinkan, Anda harus memilih sesuatu seperti ini:

class Foo
  def bar
    'Hello'
  end
end 

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

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

Ini berfungsi, jika Anda mengontrol pembuatan Fooobjek. Ubah saja setiap tempat yang membuat Foountuk bukannya membuat ExtendedFoo. Ini bekerja lebih baik jika Anda menggunakan Pola Desain Injeksi Ketergantungan , Pola Desain Metode Pabrik , Pola Desain Pabrik Abstrak atau sesuatu di sepanjang garis itu, karena dalam kasus itu, hanya ada tempat yang perlu Anda ubah.

Delegasi

Jika Anda tidak mengontrol pembuatan Fooobjek, misalnya karena mereka dibuat oleh kerangka kerja yang berada di luar kendali Anda (sepertimisalnya), maka Anda dapat menggunakan Pola Desain 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'

Pada dasarnya, pada batas sistem, di mana Fooobjek datang ke kode Anda, Anda membungkusnya ke objek lain, dan kemudian menggunakan yang objek bukan yang asli di tempat lain dalam kode Anda.

Ini menggunakan Object#DelegateClassmetode pembantu dari delegateperpustakaan di stdlib.

"Clean" Monkey Patching

Module#prepend: Mixin Prepending

Dua metode di atas perlu mengubah sistem untuk menghindari tambalan monyet. Bagian ini menunjukkan metode penambalan monyet yang disukai dan paling tidak invasif, jika mengubah sistem tidak menjadi pilihan.

Module#prependtelah ditambahkan untuk mendukung lebih atau kurang tepatnya use case ini. Module#prependmelakukan hal yang sama dengan Module#include, kecuali itu bercampur dalam mixin langsung di bawah kelas:

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'

Catatan: Saya juga menulis sedikit tentang Module#prependpertanyaan ini: Modul Ruby prepend vs deration

Warisan Mixin (rusak)

Saya telah melihat beberapa orang mencoba (dan bertanya mengapa itu tidak bekerja di sini di StackOverflow) sesuatu seperti ini, yaitu includemixin bukannya prepending:

class Foo
  def bar
    'Hello'
  end
end 

module FooExtensions
  def bar
    super + ' World'
  end
end

class Foo
  include FooExtensions
end

Sayangnya, itu tidak akan berhasil. Itu ide yang bagus, karena menggunakan warisan, yang berarti Anda bisa menggunakannya super. Namun, Module#includememasukkan mixin di atas kelas dalam hirarki warisan, yang berarti bahwa FooExtensions#bartidak akan dipanggil (dan jika yang disebut, supertidak akan benar-benar mengacu Foo#barmelainkan untuk Object#baryang tidak ada), karena Foo#barakan selalu ditemukan pertama.

Metode Pembungkus

Pertanyaan besarnya adalah: bagaimana kita bisa berpegang pada barmetode tersebut, tanpa benar-benar mempertahankan metode yang sebenarnya ? Jawabannya terletak, seperti yang sering terjadi, dalam pemrograman fungsional. Kami mendapatkan metode sebagai objek aktual , dan kami menggunakan penutupan (yaitu blok) untuk memastikan bahwa kami dan hanya kami yang memegang objek itu:

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'

Ini sangat bersih: karena old_barhanya variabel lokal, ia akan keluar dari ruang lingkup di akhir kelas, dan tidak mungkin untuk mengaksesnya dari mana saja, bahkan menggunakan refleksi! Dan karena Module#define_methodmengambil blok, dan blok menutup lingkungan leksikal sekitarnya (itulah sebabnya kami menggunakan define_methodalih-alih di defsini), itu (dan hanya itu) masih akan memiliki akses ke old_bar, bahkan setelah itu telah keluar dari ruang lingkup.

Penjelasan singkat:

old_bar = instance_method(:bar)

Di sini kita membungkus barmetode menjadi UnboundMethodobjek metode dan menugaskannya ke variabel lokal old_bar. Ini berarti, kita sekarang memiliki cara untuk bertahan barbahkan setelah ditimpa.

old_bar.bind(self)

Ini agak sulit. Pada dasarnya, di Ruby (dan di hampir semua bahasa OO berbasis pengiriman tunggal), metode terikat ke objek penerima tertentu, yang disebut selfdalam Ruby. Dengan kata lain: metode selalu tahu objek apa yang dipanggil, ia tahu apa selfitu. Tapi, kami mengambil metode ini langsung dari kelas, bagaimana ia tahu apa selfitu?

Ya, tidak, itu sebabnya kita perlu ke objek bindkita UnboundMethodterlebih dahulu, yang akan mengembalikan Methodobjek yang bisa kita panggil. ( UnboundMethodTidak dapat dipanggil, karena mereka tidak tahu apa yang harus dilakukan tanpa mengetahui mereka self.)

Dan untuk apa kita bind? Kita hanya bindmelakukannya untuk diri kita sendiri, dengan cara itu akan berperilaku persis seperti aslinya bar!

Terakhir, kita perlu memanggil dari Methodmana kembali bind. Di Ruby 1.9, ada beberapa sintaks baru yang bagus untuk itu ( .()), tetapi jika Anda menggunakan 1,8, Anda bisa menggunakan callmetode ini; .()toh itu yang akan diterjemahkan.

Berikut adalah beberapa pertanyaan lain, di mana beberapa konsep tersebut dijelaskan:

Penambalan Monyet "Kotor"

alias_method rantai

Masalah yang kita alami dengan tambalan monyet kita adalah ketika kita menimpa metode, metode itu hilang, jadi kita tidak bisa menyebutnya lagi. Jadi, mari kita buat salinan cadangan!

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'

Masalah dengan ini adalah bahwa kita sekarang telah mencemari namespace dengan old_barmetode yang berlebihan . Metode ini akan muncul di dokumentasi kami, itu akan muncul dalam penyelesaian kode di IDE kami, itu akan muncul selama refleksi. Juga, masih bisa dipanggil, tapi mungkin kita menambalnya, karena kita tidak suka dengan perilakunya, jadi kita mungkin tidak ingin orang lain menyebutnya.

Terlepas dari kenyataan bahwa ini memiliki beberapa properti yang tidak diinginkan, sayangnya telah dipopulerkan melalui AciveSupport Module#alias_method_chain.

Samping: Perbaikan

Jika Anda hanya perlu perilaku berbeda di beberapa tempat tertentu dan tidak di seluruh sistem, Anda dapat menggunakan Penyempitan untuk membatasi tambalan monyet ke lingkup tertentu. Saya akan menunjukkannya di sini menggunakan Module#prependcontoh dari atas:

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!

Anda dapat melihat contoh yang lebih canggih dari menggunakan Penyempitan dalam pertanyaan ini: Bagaimana mengaktifkan tambalan monyet untuk metode tertentu?


Gagasan yang ditinggalkan

Sebelum komunitas Ruby diselesaikan Module#prepend, ada beberapa ide berbeda yang beredar yang kadang-kadang Anda lihat dirujuk dalam diskusi yang lebih lama. Semua ini digolongkan oleh Module#prepend.

Metode Combinators

Satu ide adalah ide kombinator metode dari CLOS. Ini pada dasarnya adalah versi yang sangat ringan dari subset Pemrograman Berorientasi Aspek.

Menggunakan sintaks seperti

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

Anda akan dapat "menghubungkan ke" pelaksanaan barmetode.

Namun tidak begitu jelas apakah dan bagaimana Anda mendapatkan akses ke barnilai pengembalian di dalamnya bar:after. Mungkin kita bisa (ab) menggunakan superkata kunci?

class Foo
  def bar
    'Hello'
  end
end 

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

Penggantian

Combinator yang sebelumnya setara prependdengan mixin dengan metode overriding yang memanggil superdi akhir metode. Demikian juga, kombinator setelah adalah setara dengan prepending mixin dengan metode utama yang memanggil superpada awal metode.

Anda juga dapat melakukan hal-hal sebelum dan setelah menelepon super, Anda dapat memanggil superbeberapa kali, dan keduanya mengambil dan memanipulasi supernilai pengembalian, menjadikannya prependlebih kuat daripada kombinator metode.

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

dan

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 kata kunci

Gagasan ini menambahkan kata kunci baru yang mirip dengan super, yang memungkinkan Anda memanggil metode yang ditimpa dengan cara yang sama supermemungkinkan Anda memanggil metode yang diganti :

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar
    old + ' World'
  end
end

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

Masalah utama dengan ini adalah bahwa itu tidak kompatibel ke belakang: jika Anda memiliki metode yang dipanggil old, Anda tidak akan lagi dapat menyebutnya!

Penggantian

superdalam metode override dalam prepended mixin pada dasarnya sama dengan olddi proposal ini.

redef kata kunci

Mirip dengan di atas, tetapi alih-alih menambahkan kata kunci baru untuk memanggil metode yang ditimpa dan membiarkannya defsendiri, kami menambahkan kata kunci baru untuk metode mendefinisikan ulang . Ini kompatibel dengan mundur, karena sintaksis saat ini ilegal:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    old + ' World'
  end
end

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

Alih-alih menambahkan dua kata kunci baru, kami juga dapat mendefinisikan kembali makna superdi dalam redef:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    super + ' World'
  end
end

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

Penggantian

redefining suatu metode sama dengan mengganti metode dalam prependmixin ed. superdalam metode utama berperilaku seperti superatau olddalam proposal ini.

Jörg W Mittag
sumber
@ Jörg W Mittag, apakah metode pendekatan metode pembungkus aman? Apa yang terjadi ketika dua utas bersamaan memanggil variabel yang bindsama old_method?
Harish Shetty
1
@KandadaBoggu: Saya mencoba mencari tahu apa yang sebenarnya Anda maksudkan dengan itu :-) Namun, saya cukup yakin itu tidak kalah amannya daripada metaprogramming lainnya di Ruby. Secara khusus, setiap panggilan ke UnboundMethod#bindakan mengembalikan yang baru, berbeda Method, jadi, saya tidak melihat konflik yang muncul, terlepas dari apakah Anda menyebutnya dua kali berturut-turut atau dua kali secara bersamaan dari utas berbeda.
Jörg W Mittag
1
Sedang mencari penjelasan tentang penambalan seperti ini sejak saya mulai menggunakan ruby ​​dan rails. Jawaban bagus! Satu-satunya hal yang hilang bagi saya adalah catatan tentang class_eval vs membuka kembali kelas. Ini dia: stackoverflow.com/a/10304721/188462
Eugene
1
Ruby 2.0 memiliki penyempitan blog.wyeworks.com/2012/8/3/ruby-refinements-landed-in-trunk
NARKOZ
5
Di mana Anda menemukan olddan redef? 2.0.0 saya tidak memilikinya. Ah, sulit untuk tidak melewatkan ide-ide lain yang bersaing yang tidak berhasil masuk ke dalam Ruby adalah:
Nakilon
12

Lihatlah metode aliasing, ini semacam mengubah nama metode menjadi nama baru.

Untuk informasi lebih lanjut dan titik awal lihat artikel metode penggantian ini (terutama bagian pertama). The Ruby API docs , juga menyediakan (kurang rumit) misalnya.

Veer
sumber
-1

Kelas yang akan membuat override harus di-reload setelah kelas yang berisi metode asli, sehingga requiredi file yang akan membuat overrride.

rplaurindo
sumber