Apa gunanya metode accept () dalam pola Pengunjung?

88

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."

Val
sumber
Dikatakan "ketika panggilan pengunjung diterima, panggilan dikirim berdasarkan jenis panggilan. Kemudian panggilan panggilan kembali jenis metode kunjungan khusus pengunjung, dan panggilan ini dikirim berdasarkan jenis pengunjung yang sebenarnya." Dengan kata lain, itu menyatakan hal yang membingungkan saya. Untuk alasan ini, dapatkah Anda lebih spesifik?
Val

Jawaban:

155

Pola visit/ acceptkonstruksi 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 Nodetipe dasar , selanjutnya disebut sebagai Node. Secara naluriah, kami akan menulisnya seperti ini:

Node root = GetTreeRoot();
new MyVisitor().visit(root);

Di sinilah letak masalahnya. Jika MyVisitorkelas 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 MyVisitoryang 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 Nodeyang 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 TrainElementimplementasi 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 thisadalah aTrainNode . Ini adalah potongan informasi tambahan penting yang tidak disadari oleh compiler dalam cakupan pemanggil kami: di sana, yang diketahui roothanyalah 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: MyVisitorimplementasi 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 leftatau rightdi visit()metode. Parser kami kemungkinan besar juga hanya akan mengembalikan objek bertipe Nodeyang 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 IVisitorantarmuka, 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.

atanamir
sumber
5
Item Java Efektif # 41 mencakup peringatan ini: " hindari situasi di mana kumpulan parameter yang sama dapat diteruskan ke kelebihan muatan yang berbeda dengan penambahan cast. " accept()Metode menjadi perlu saat peringatan ini dilanggar di Pengunjung.
jaco0646
" Biasanya ketika pola pengunjung digunakan, hierarki objek terlibat di mana semua node diturunkan dari jenis node dasar " ini sama sekali tidak diperlukan dalam C ++. Lihat Boost.Variant, Eggs.Variant
Jean-Michaël Celerier
Tampaknya bagi saya bahwa di java kami tidak benar-benar membutuhkan metode terima karena di java kami selalu memanggil metode tipe yang paling spesifik
Gilad Baruchian
1
Wow, ini penjelasan yang luar biasa. Mencerahkan untuk melihat bahwa semua bayangan pola adalah karena keterbatasan kompiler, dan sekarang mengungkapkan terima kasih yang jelas kepada Anda.
Alfonso Nishikawa
@GiladBaruchian, kompilator menghasilkan panggilan ke metode tipe paling spesifik yang dapat ditentukan kompilator .
mmw
15

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.

interface IAcceptVisitor<T> {
  void Accept(IVisit<T> visitor);
}
class HierarchyNode : IAcceptVisitor<HierarchyNode> {
  public void Accept(IVisit<T> visitor) {
    visitor.visit(this);
    foreach(var n in this.children)
      n.Accept(visitor);
  }

  private IEnumerable<HierarchyNode> children;
  ....
}

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 Visitberisi logika untuk diterapkan ke node. Metode node Acceptberisi 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.

George Mauer
sumber
7
Penjelasan Anda tidak menjelaskan mengapa ini harus menjadi tanggung jawab Node daripada metode kunjungan yang sesuai Pengunjung () untuk mengulang anak-anak? Apakah maksud Anda ide utamanya adalah berbagi kode hierarki traversal ketika kita membutuhkan pola kunjungan yang sama untuk pengunjung yang berbeda? Saya tidak melihat petunjuk apa pun dari kertas yang direkomendasikan.
Val
1
Mengatakan bahwa menerima itu baik untuk perjalanan rutin adalah wajar dan bermanfaat bagi masyarakat umum. Tapi, saya mengambil contoh saya dari orang lain "Saya tidak bisa memahami pola Pengunjung sampai saya membaca andymaleh.blogspot.com/2008/04/… ". Baik contoh ini maupun Wikipedia atau jawaban lain tidak menyebutkan keuntungan navigasi. Namun demikian, mereka semua menuntut penerimaan bodoh ini (). Itulah mengapa menanyakan pertanyaan saya: Mengapa?
Val
1
@Val - apa maksudmu? Saya tidak yakin apa yang Anda tanyakan. Saya tidak dapat berbicara untuk artikel lain karena orang-orang itu memiliki pandangan berbeda tentang hal ini tetapi saya ragu bahwa kami tidak setuju. Secara umum dalam komputasi banyak masalah yang dapat dipetakan ke jaringan, jadi penggunaan mungkin tidak ada hubungannya dengan grafik di permukaan tetapi sebenarnya masalah yang sangat mirip.
George Mauer
1
Memberikan contoh di mana beberapa metode mungkin berguna tidak menjawab pertanyaan mengapa metode itu wajib. Karena navigasi tidak selalu diperlukan, metode accept () tidak selalu baik untuk kunjungan. Karena itu, kita harus dapat mencapai tujuan kita tanpa itu. Namun demikian, itu wajib. Ini berarti bahwa ada alasan yang lebih kuat untuk memperkenalkan accept () ke dalam setiap derai pengunjung daripada "terkadang berguna". Apa yang tidak jelas dalam pertanyaan saya? Jika Anda tidak mencoba untuk memahami mengapa Wikipedia mencari cara untuk menyingkirkan menerima, Anda tidak tertarik untuk memahami pertanyaan saya.
Val
1
@Val Makalah yang mereka tautkan ke "Esensi Pola Pengunjung" mencatat pemisahan yang sama antara navigasi dan operasi dalam abstraknya seperti yang saya berikan. Mereka hanya mengatakan bahwa implementasi GOF (yang Anda tanyakan) memiliki beberapa batasan dan gangguan yang dapat dihilangkan dengan menggunakan refleksi - jadi mereka memperkenalkan pola Walkabout. Hal ini tentunya berguna dan dapat melakukan banyak hal yang sama yang dapat dilakukan pengunjung tetapi banyak kode yang cukup canggih dan (sepintas) kehilangan beberapa manfaat dari keamanan jenis. Ini adalah alat untuk kotak peralatan tetapi yang lebih berat daripada pengunjung
George Mauer
0

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:

  1. Itu dapat mengunci rekaman, memanggil fungsi yang disediakan, dan kemudian membuka kunci rekaman; record bisa dikunci selamanya jika fungsi yang disediakan jatuh ke loop tanpa akhir, tetapi jika fungsi yang disediakan mengembalikan atau melontarkan pengecualian, record akan dibuka (mungkin masuk akal untuk menandai record tidak valid jika fungsi melempar pengecualian; meninggalkan terkunci mungkin bukan ide yang baik). Perhatikan bahwa penting bahwa jika fungsi yang dipanggil mencoba memperoleh kunci lain, kebuntuan dapat terjadi.
  2. Pada beberapa platform, ini dapat melewati lokasi penyimpanan yang menahan string sebagai parameter 'ref'. Fungsi itu kemudian dapat menyalin string, menghitung string baru berdasarkan string yang disalin, mencoba CompareExchange string lama ke yang baru, dan ulangi seluruh proses jika CompareExchange gagal.
  3. Itu dapat membuat salinan string, memanggil fungsi yang disediakan pada string, kemudian menggunakan CompareExchange itu sendiri untuk mencoba memperbarui yang asli, dan mengulangi seluruh proses jika CompareExchange gagal.

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.

supercat
sumber
2
1. Kunjungan menyiratkan bahwa Anda hanya memiliki akses ke metode umum kunjungan sehingga perlu membuat kunci internal dapat diakses untuk umum agar berguna dengan Pengunjung. 2 / Tak satu pun dari contoh yang pernah saya lihat sebelumnya menyiratkan bahwa Pengunjung seharusnya digunakan untuk mengubah status dikunjungi. 3. "Dengan VisitorPattern tradisional, seseorang hanya dapat menentukan saat kita memasuki sebuah simpul. Kita tidak tahu apakah kita telah meninggalkan simpul sebelumnya sebelum kita memasuki simpul saat ini." Bagaimana Anda membuka kunci dengan hanya mengunjungi alih-alih visitEnter dan visitLeave? Akhirnya, saya bertanya tentang aplikasi accpet () daripada Pengunjung.
Val
Mungkin saya tidak sepenuhnya memahami terminologi untuk pola, tetapi "pola pengunjung" tampaknya menyerupai pendekatan yang saya gunakan di mana X melewati Y delegasi, di mana Y kemudian dapat menyampaikan informasi yang hanya perlu valid sebagai selama delegasi berjalan. Mungkin pola itu punya nama lain?
supercat
2
Ini adalah aplikasi pola pengunjung yang menarik untuk masalah tertentu tetapi tidak menggambarkan pola itu sendiri atau menjawab pertanyaan asli. "Dalam kasus di mana tidak ada pembersihan yang diperlukan, pola pengunjung tidak terlalu berguna." Klaim ini jelas salah dan hanya berkaitan dengan masalah spesifik Anda dan bukan polanya secara umum.
Tony O'Hagan
0

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.

andrew pate
sumber
-1

Contoh yang bagus adalah dalam kompilasi kode sumber:

interface CompilingVisitor {
   build(SourceFile source);
}

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:

interface CompilingVisitor {
   build(JavaSourceFile source);
   build(RubySourceFile source);
   build(XMLSourceFile source);
}

Itu tergantung pada konteks dan bagian mana dari sistem yang ingin Anda perluas.

Garrett Hall
sumber
Ironisnya adalah VisitorPattern menawarkan kita untuk menggunakan pola yang buruk. Dikatakan bahwa kita harus mendefinisikan metode kunjungan untuk setiap jenis node yang akan dikunjungi. Kedua, tidak jelas apakah contoh Anda baik atau buruk? Bagaimana mereka terkait dengan pertanyaan saya?
Val