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 (sepertiruby-on-railsmisalnya), 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
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"
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.
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.