Ada banyak pembicaraan tentang pemisahan algoritma dari kelas. Tapi, satu hal tetap tidak dijelaskan.
Mereka menggunakan pengunjung seperti ini
abstract class Expr {
public <T> T accept(Visitor<T> visitor) {visitor.visit(this);}
}
class ExprVisitor extends Visitor{
public Integer visit(Num num) {
return num.value;
}
public Integer visit(Sum sum) {
return sum.getLeft().accept(this) + sum.getRight().accept(this);
}
public Integer visit(Prod prod) {
return prod.getLeft().accept(this) * prod.getRight().accept(this);
}
Alih-alih memanggil kunjungan (elemen) secara langsung, Pengunjung meminta elemen untuk memanggil metode kunjungannya. Ini bertentangan dengan gagasan yang dinyatakan tentang ketidaksadaran kelas tentang pengunjung.
PS1 Tolong jelaskan dengan kata-kata Anda sendiri atau tunjukkan penjelasan yang tepat. Karena dua tanggapan yang saya dapatkan mengacu pada sesuatu yang umum dan tidak pasti.
PS2 Dugaan saya: Karena getLeft()
mengembalikan dasar Expression
, memanggil visit(getLeft())
akan menghasilkan visit(Expression)
, sedangkan getLeft()
memanggil visit(this)
akan menghasilkan permintaan kunjungan lain yang lebih sesuai. Jadi, accept()
lakukan konversi tipe (alias casting).
Pencocokan Pola PS3 Scala = Pola Pengunjung pada Steroid menunjukkan betapa lebih sederhana pola Pengunjung tanpa metode terima. Wikipedia menambahkan pernyataan ini : dengan menautkan makalah yang menunjukkan "bahwa accept()
metode tidak diperlukan ketika refleksi tersedia; memperkenalkan istilah 'Walkabout' untuk teknik tersebut."
Jawaban:
Pola
visit
/accept
konstruksi pengunjung adalah kejahatan yang diperlukan karena semantik bahasa mirip-C (C #, Java, dll.). Sasaran dari pola pengunjung adalah menggunakan pengiriman ganda untuk merutekan panggilan Anda seperti yang Anda harapkan dari membaca kode.Biasanya ketika pola pengunjung digunakan, hierarki objek terlibat di mana semua node berasal dari
Node
tipe dasar , selanjutnya disebut sebagaiNode
. Secara naluriah, kami akan menulisnya seperti ini:Di sinilah letak masalahnya. Jika
MyVisitor
kelas kita didefinisikan seperti berikut:Jika, pada waktu proses, terlepas dari jenis sebenarnya
root
, panggilan kita akan mengalami kelebihan bebanvisit(Node node)
. Ini akan benar untuk semua variabel yang dideklarasikan tipeNode
. Kenapa ini? Karena Java dan bahasa mirip C lainnya hanya mempertimbangkan tipe statis , atau tipe variabel yang dideklarasikan, dari parameter saat memutuskan overload mana yang akan dipanggil. Java tidak mengambil langkah ekstra untuk menanyakan, untuk setiap pemanggilan metode, pada waktu proses, "Oke, apa tipe dinamisnyaroot
? Oh, begitu. Ini aTrainNode
. Mari kita lihat apakah ada metodeMyVisitor
yang menerima parameter tipeTrainNode
... ". Kompilator, pada waktu kompilasi, menentukan metode mana yang akan dipanggil. (Jika Java memang memeriksa tipe dinamis argumen, kinerjanya akan sangat buruk.)Java memang memberi kita satu alat untuk memperhitungkan jenis runtime (yaitu dinamis) dari suatu objek ketika sebuah metode disebut - metode pengiriman virtual . Saat kita memanggil metode virtual, panggilan tersebut sebenarnya menuju ke tabel di memori yang terdiri dari penunjuk fungsi. Setiap jenis memiliki tabel. Jika metode tertentu diganti oleh kelas, entri tabel fungsi kelas itu akan berisi alamat fungsi yang diganti. Jika kelas tidak menimpa metode, itu akan berisi pointer ke implementasi kelas dasar. Ini masih menimbulkan overhead kinerja (setiap panggilan metode pada dasarnya akan mendereferensi dua petunjuk: satu menunjuk ke tabel fungsi tipe, dan satu lagi fungsi itu sendiri), tetapi masih lebih cepat daripada harus memeriksa tipe parameter.
Tujuan dari pola pengunjung adalah untuk mencapai pengiriman ganda - tidak hanya jenis target panggilan yang dipertimbangkan (
MyVisitor
, melalui metode virtual), tetapi juga jenis parameternya (jenis apaNode
yang kita lihat)? Pola Pengunjung memungkinkan kita melakukan ini dengan kombinasivisit
/accept
.Dengan mengubah baris kami menjadi ini:
Kita bisa mendapatkan apa yang kita inginkan: melalui pengiriman metode virtual, kita memasukkan panggilan accept () yang benar seperti yang diterapkan oleh subkelas - dalam contoh kita dengan
TrainElement
, kita akan memasukkanTrainElement
implementasiaccept()
:Apa yang diketahui kompilator pada saat ini, di dalam cakupan
TrainNode
'saccept
? Ia tahu bahwa tipe statisthis
adalah aTrainNode
. Ini adalah potongan informasi tambahan penting yang tidak disadari oleh compiler dalam cakupan pemanggil kami: di sana, yang diketahuiroot
hanyalah bahwa itu adalah aNode
. Sekarang kompilator tahu bahwathis
(root
) bukan hanya aNode
, tapi sebenarnya aTrainNode
. Karena, satu baris ditemukan di dalamaccept()
:v.visit(this)
, berarti sesuatu yang lain sama sekali. Kompilator sekarang akan mencari kelebihan darivisit()
yang membutuhkanTrainNode
. Jika tidak dapat menemukannya, itu akan mengkompilasi panggilan ke kelebihan beban yang membutuhkan fileNode
. Jika tidak ada, Anda akan mendapatkan kesalahan kompilasi (kecuali jika Anda mengalami kelebihan bebanobject
). Eksekusi dengan demikian akan memasuki apa yang kita inginkan selama ini:MyVisitor
implementasivisit(TrainNode e)
. Tidak diperlukan gips, dan yang terpenting, tidak diperlukan refleksi. Jadi, overhead mekanisme ini agak rendah: hanya terdiri dari referensi penunjuk dan tidak ada yang lain.Anda benar dalam pertanyaan Anda - kami dapat menggunakan pemeran dan mendapatkan perilaku yang benar. Namun, seringkali, kita bahkan tidak mengetahui tipe Node apa itu. Ambil contoh kasus dari hierarki berikut:
Dan kami sedang menulis kompilator sederhana yang mem-parsing file sumber dan menghasilkan hierarki objek yang sesuai dengan spesifikasi di atas. Jika kami menulis juru bahasa untuk hierarki yang diterapkan sebagai Pengunjung:
Casting tidak akan mendapatkan kita sangat jauh, karena kita tidak tahu jenis
left
atauright
divisit()
metode. Parser kami kemungkinan besar juga hanya akan mengembalikan objek bertipeNode
yang menunjuk ke root hierarki juga, jadi kami juga tidak dapat mentransmisikannya dengan aman. Jadi penerjemah sederhana kami dapat terlihat seperti:Pola pengunjung memungkinkan kita melakukan sesuatu yang sangat berguna: dengan hierarki objek, pola ini memungkinkan kita untuk membuat operasi modular yang beroperasi di atas hierarki tanpa perlu meletakkan kode di kelas hierarki itu sendiri. Pola pengunjung digunakan secara luas, misalnya dalam konstruksi penyusun. Mengingat pohon sintaks dari program tertentu, banyak pengunjung ditulis yang beroperasi pada pohon itu: pemeriksaan jenis, pengoptimalan, emisi kode mesin semuanya biasanya diterapkan sebagai pengunjung yang berbeda. Dalam kasus pengunjung pengoptimalan, ia bahkan dapat mengeluarkan pohon sintaks baru yang diberikan pohon masukan.
Tentu saja ada kekurangannya: jika kita menambahkan tipe baru ke dalam hierarki, kita juga perlu menambahkan
visit()
metode untuk tipe baru tersebut keIVisitor
antarmuka, dan membuat implementasi rintisan (atau lengkap) di semua pengunjung kita. Kami juga perlu menambahkanaccept()
metode juga, untuk alasan yang dijelaskan di atas. Jika kinerja tidak terlalu berarti bagi Anda, ada solusi untuk menulis pengunjung tanpa memerlukannyaaccept()
, tetapi biasanya melibatkan refleksi dan dengan demikian dapat menimbulkan biaya overhead yang cukup besar.sumber
accept()
Metode menjadi perlu saat peringatan ini dilanggar di Pengunjung.Tentu saja itu akan konyol jika hanya itu cara Terima diterapkan.
Tapi ternyata tidak.
Misalnya, pengunjung benar-benar berguna ketika berhadapan dengan hierarki di mana implementasi node non-terminal mungkin seperti ini.
Kamu melihat? Apa yang Anda gambarkan sebagai bodoh adalah yang solusi untuk melintasi hirarki.
Berikut adalah artikel yang lebih panjang dan mendalam yang membuat saya memahami pengunjung .
Edit: Untuk memperjelas: Metode pengunjung
Visit
berisi logika untuk diterapkan ke node. Metode nodeAccept
berisi logika tentang cara menavigasi ke node yang berdekatan. Kasus di mana Anda hanya melakukan pengiriman ganda adalah kasus khusus di mana tidak ada node yang berdekatan untuk dinavigasi.sumber
Tujuan dari pola Pengunjung adalah untuk memastikan bahwa objek mengetahui kapan pengunjung selesai dengan mereka dan telah pergi, sehingga kelas dapat melakukan pembersihan yang diperlukan sesudahnya. Ini juga memungkinkan kelas untuk mengekspos internal mereka "sementara" sebagai parameter 'ref', dan mengetahui bahwa internal tidak akan lagi diekspos setelah pengunjung pergi. Dalam kasus di mana tidak ada pembersihan yang diperlukan, pola pengunjung tidak terlalu berguna. Kelas yang tidak melakukan hal-hal ini mungkin tidak mendapatkan keuntungan dari pola pengunjung, tetapi kode yang ditulis untuk menggunakan pola pengunjung akan dapat digunakan dengan kelas yang akan datang yang mungkin memerlukan pembersihan setelah akses.
Misalnya, seseorang memiliki struktur data yang menyimpan banyak string yang harus diperbarui secara atomik, tetapi kelas yang memegang struktur data tersebut tidak tahu persis jenis pembaruan atom apa yang harus dilakukan (misalnya jika satu utas ingin mengganti semua kemunculan " X ", sementara utas lain ingin mengganti urutan digit apa pun dengan urutan yang secara numerik lebih tinggi, operasi kedua utas harus berhasil; jika setiap utas hanya membaca string, melakukan pembaruannya, dan menulisnya kembali, utas kedua untuk menulis kembali stringnya akan menimpa yang pertama). Salah satu cara untuk melakukannya adalah dengan meminta setiap utas mendapatkan kunci, menjalankan operasinya, dan melepaskan kunci. Sayangnya, jika kunci terbuka seperti itu,
Pola Pengunjung menawarkan (setidaknya) tiga pendekatan untuk menghindari masalah itu:
Tanpa pola pengunjung, melakukan pembaruan atomik memerlukan penguncian dan risiko kegagalan jika perangkat lunak pemanggil gagal mengikuti protokol penguncian / pembukaan kunci yang ketat. Dengan pola Pengunjung, pembaruan atomik dapat dilakukan dengan relatif aman.
sumber
Semua kelas yang memerlukan modifikasi harus menerapkan metode 'terima'. Klien memanggil metode terima ini untuk melakukan beberapa tindakan baru pada kelompok kelas tersebut sehingga memperluas fungsionalitasnya. Klien dapat menggunakan metode terima yang satu ini untuk melakukan berbagai tindakan baru dengan meneruskan kelas pengunjung yang berbeda untuk setiap tindakan tertentu. Kelas pengunjung berisi beberapa metode kunjungan yang diganti yang menentukan cara mencapai tindakan spesifik yang sama untuk setiap kelas dalam keluarga. Metode kunjungan ini melewati sebuah contoh untuk bekerja.
Pengunjung berguna jika Anda sering menambahkan, mengubah, atau menghapus fungsionalitas ke keluarga kelas yang stabil karena setiap item fungsionalitas didefinisikan secara terpisah di setiap kelas pengunjung dan kelas itu sendiri tidak perlu diubah. Jika kelompok kelas tidak stabil maka pola pengunjung mungkin kurang bermanfaat, karena banyak pengunjung perlu diubah setiap kali kelas ditambahkan atau dihapus.
sumber
Contoh yang bagus adalah dalam kompilasi kode sumber:
Klien dapat menerapkan
JavaBuilder
,RubyBuilder
,XMLValidator
, dll dan pelaksanaan untuk mengumpulkan dan mengunjungi semua file sumber dalam proyek tidak perlu perubahan.Ini akan menjadi pola yang buruk jika Anda memiliki kelas terpisah untuk setiap jenis file sumber:
Itu tergantung pada konteks dan bagian mana dari sistem yang ingin Anda perluas.
sumber