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 Foo
objek. Ubah saja setiap tempat yang membuat Foo
untuk 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 Foo
objek, 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 Foo
objek 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#DelegateClass
metode pembantu dari delegate
perpustakaan 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#prepend
telah ditambahkan untuk mendukung lebih atau kurang tepatnya use case ini. Module#prepend
melakukan 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#prepend
pertanyaan 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 include
mixin bukannya prepend
ing:
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#include
memasukkan mixin di atas kelas dalam hirarki warisan, yang berarti bahwa FooExtensions#bar
tidak akan dipanggil (dan jika yang disebut, super
tidak akan benar-benar mengacu Foo#bar
melainkan untuk Object#bar
yang tidak ada), karena Foo#bar
akan selalu ditemukan pertama.
Metode Pembungkus
Pertanyaan besarnya adalah: bagaimana kita bisa berpegang pada bar
metode 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_bar
hanya 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_method
mengambil blok, dan blok menutup lingkungan leksikal sekitarnya (itulah sebabnya kami menggunakan define_method
alih-alih di def
sini), 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 bar
metode menjadi UnboundMethod
objek metode dan menugaskannya ke variabel lokal old_bar
. Ini berarti, kita sekarang memiliki cara untuk bertahan bar
bahkan 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 self
dalam Ruby. Dengan kata lain: metode selalu tahu objek apa yang dipanggil, ia tahu apa self
itu. Tapi, kami mengambil metode ini langsung dari kelas, bagaimana ia tahu apa self
itu?
Ya, tidak, itu sebabnya kita perlu ke objek bind
kita UnboundMethod
terlebih dahulu, yang akan mengembalikan Method
objek yang bisa kita panggil. ( UnboundMethod
Tidak dapat dipanggil, karena mereka tidak tahu apa yang harus dilakukan tanpa mengetahui mereka self
.)
Dan untuk apa kita bind
? Kita hanya bind
melakukannya untuk diri kita sendiri, dengan cara itu akan berperilaku persis seperti aslinya bar
!
Terakhir, kita perlu memanggil dari Method
mana kembali bind
. Di Ruby 1.9, ada beberapa sintaks baru yang bagus untuk itu ( .()
), tetapi jika Anda menggunakan 1,8, Anda bisa menggunakan call
metode 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_bar
metode 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#prepend
contoh 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 bar
metode.
Namun tidak begitu jelas apakah dan bagaimana Anda mendapatkan akses ke bar
nilai pengembalian di dalamnya bar:after
. Mungkin kita bisa (ab) menggunakan super
kata kunci?
class Foo
def bar
'Hello'
end
end
class Foo
def bar:after
super + ' World'
end
end
Penggantian
Combinator yang sebelumnya setara prepend
dengan mixin dengan metode overriding yang memanggil super
di akhir metode. Demikian juga, kombinator setelah adalah setara dengan prepend
ing mixin dengan metode utama yang memanggil super
pada awal metode.
Anda juga dapat melakukan hal-hal sebelum dan setelah menelepon super
, Anda dapat memanggil super
beberapa kali, dan keduanya mengambil dan memanipulasi super
nilai pengembalian, menjadikannya prepend
lebih 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 super
memungkinkan 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
super
dalam metode override dalam prepend
ed mixin pada dasarnya sama dengan old
di proposal ini.
redef
kata kunci
Mirip dengan di atas, tetapi alih-alih menambahkan kata kunci baru untuk memanggil metode yang ditimpa dan membiarkannya def
sendiri, 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 super
di dalam redef
:
class Foo
def bar
'Hello'
end
end
class Foo
redef bar
super + ' World'
end
end
Foo.new.bar # => 'Hello World'
Penggantian
redef
ining suatu metode sama dengan mengganti metode dalam prepend
mixin ed. super
dalam metode utama berperilaku seperti super
atau old
dalam proposal ini.