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 sebagai Node
. Secara naluriah, kami akan menulisnya seperti ini:
Node root = GetTreeRoot();
new MyVisitor().visit(root);
Di sinilah letak masalahnya. Jika MyVisitor
kelas kita didefinisikan seperti berikut:
class MyVisitor implements IVisitor {
void visit(CarNode node);
void visit(TrainNode node);
void visit(PlaneNode node);
void visit(Node node);
}
Jika, pada waktu proses, terlepas dari jenis sebenarnyaroot
, panggilan kita akan mengalami kelebihan beban visit(Node node)
. Ini akan benar untuk semua variabel yang dideklarasikan tipe Node
. 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 dinamisnya root
? Oh, begitu. Ini a TrainNode
. Mari kita lihat apakah ada metode MyVisitor
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 apa Node
yang kita lihat)? Pola Pengunjung memungkinkan kita melakukan ini dengan kombinasi visit
/ accept
.
Dengan mengubah baris kami menjadi ini:
root.accept(new MyVisitor());
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 memasukkan TrainElement
implementasi accept()
:
class TrainNode extends Node implements IVisitable {
void accept(IVisitor v) {
v.visit(this);
}
}
Apa yang diketahui kompilator pada saat ini, di dalam cakupan TrainNode
's accept
? Ia tahu bahwa tipe statis this
adalah aTrainNode
. Ini adalah potongan informasi tambahan penting yang tidak disadari oleh compiler dalam cakupan pemanggil kami: di sana, yang diketahui root
hanyalah bahwa itu adalah a Node
. Sekarang kompilator tahu bahwa this
( root
) bukan hanya a Node
, tapi sebenarnya a TrainNode
. Karena, satu baris ditemukan di dalam accept()
: v.visit(this)
, berarti sesuatu yang lain sama sekali. Kompilator sekarang akan mencari kelebihan dari visit()
yang membutuhkan TrainNode
. 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 beban object
). Eksekusi dengan demikian akan memasuki apa yang kita inginkan selama ini: MyVisitor
implementasi visit(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:
abstract class Node { ... }
abstract class BinaryNode extends Node { Node left, right; }
abstract class AdditionNode extends BinaryNode { }
abstract class MultiplicationNode extends BinaryNode { }
abstract class LiteralNode { int value; }
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:
class Interpreter implements IVisitor<int> {
int visit(AdditionNode n) {
int left = n.left.accept(this);
int right = n.right.accept(this);
return left + right;
}
int visit(MultiplicationNode n) {
int left = n.left.accept(this);
int right = n.right.accept(this);
return left * right;
}
int visit(LiteralNode n) {
return n.value;
}
}
Casting tidak akan mendapatkan kita sangat jauh, karena kita tidak tahu jenis left
atau right
di visit()
metode. Parser kami kemungkinan besar juga hanya akan mengembalikan objek bertipe Node
yang menunjuk ke root hierarki juga, jadi kami juga tidak dapat mentransmisikannya dengan aman. Jadi penerjemah sederhana kami dapat terlihat seperti:
Node program = parse(args[0]);
int result = program.accept(new Interpreter());
System.out.println("Output: " + result);
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 ke IVisitor
antarmuka, dan membuat implementasi rintisan (atau lengkap) di semua pengunjung kita. Kami juga perlu menambahkan accept()
metode juga, untuk alasan yang dijelaskan di atas. Jika kinerja tidak terlalu berarti bagi Anda, ada solusi untuk menulis pengunjung tanpa memerlukannya accept()
, tetapi biasanya melibatkan refleksi dan dengan demikian dapat menimbulkan biaya overhead yang cukup besar.