Tip Java 98: Merefleksikan pola desain Pengunjung

Koleksi biasanya digunakan dalam pemrograman berorientasi objek dan sering kali menimbulkan pertanyaan terkait kode. Misalnya, "Bagaimana Anda melakukan operasi di sekumpulan objek yang berbeda?"

Salah satu pendekatannya adalah melakukan iterasi melalui setiap elemen dalam koleksi dan kemudian melakukan sesuatu yang spesifik untuk setiap elemen, berdasarkan kelasnya. Itu bisa sangat rumit, terutama jika Anda tidak tahu jenis objek apa yang ada dalam koleksi. Jika Anda ingin mencetak elemen dalam koleksi, Anda dapat menulis metode seperti ini:

public void messyPrintCollection (Koleksi koleksi) {Iterator iterator = collection.iterator () sementara (iterator.hasNext ()) System.out.println (iterator.next (). toString ())} 

Sepertinya itu cukup sederhana. Anda cukup memanggil Object.toString()metode dan mencetak objeknya, bukan? Bagaimana jika, misalnya, Anda memiliki vektor hashtable? Kemudian segalanya mulai menjadi lebih rumit. Anda harus memeriksa jenis objek yang dikembalikan dari koleksi:

public void messyPrintCollection (Collection collection) {Iterator iterator = collection.iterator () while (iterator.hasNext ()) {Object o = iterator.next (); if (o instanceof Collection) messyPrintCollection ((Collection) o); lain System.out.println (o.toString ()); }}

Oke, jadi sekarang Anda telah menangani koleksi bersarang, tapi bagaimana dengan objek lain yang tidak mengembalikan Stringyang Anda butuhkan dari mereka? Bagaimana jika Anda ingin menambahkan tanda kutip di sekitar Stringobjek dan menambahkan f setelah Floatobjek? Kode menjadi lebih kompleks:

public void messyPrintCollection (Collection collection) {Iterator iterator = collection.iterator () while (iterator.hasNext ()) {Object o = iterator.next (); if (o instanceof Collection) messyPrintCollection ((Collection) o); lain jika (o turunan dari String) System.out.println ("'" + o.toString () + "'"); lain jika (o instanceof Float) System.out.println (o.toString () + "f"); lain System.out.println (o.toString ()); }}

Anda dapat melihat bahwa segala sesuatunya dapat mulai menjadi rumit dengan sangat cepat. Anda tidak ingin sepotong kode dengan daftar besar pernyataan if-else! Bagaimana Anda menghindarinya? Pola Pengunjung datang untuk menyelamatkan.

Untuk menerapkan pola Pengunjung, Anda membuat Visitorantarmuka untuk pengunjung, dan Visitableantarmuka untuk koleksi yang akan dikunjungi. Anda kemudian memiliki kelas konkret yang mengimplementasikan antarmuka Visitordan Visitable. Kedua antarmuka terlihat seperti ini:

antarmuka publik Pengunjung {public void visitCollection (Koleksi koleksi); public void visitString (String string); public void visitFloat (Float float); } antarmuka publik Dapat dikunjungi {public void accept (Pengunjung pengunjung); }

Untuk beton String, Anda mungkin memiliki:

public class VisitableString mengimplementasikan Visitable {private String value; VisitableString publik (String string) {value = string; } public void accept (Pengunjung pengunjung) {visitor.visitString (this); }}

Dalam metode terima, Anda memanggil metode pengunjung yang benar untuk thisjenis:

visitor.visitString (ini) 

Itu memungkinkan Anda mengimplementasikan beton Visitorsebagai berikut:

public class PrintVisitor mengimplementasikan Pengunjung {public void visitCollection (Collection collection) {Iterator iterator = collection.iterator () while (iterator.hasNext ()) {Object o = iterator.next (); if (o instanceof Visitable) ((Visitable) o) .accept (this); } public void visitString (String string) {System.out.println ("'" + string + "'"); } public void visitFloat (Float float) {System.out.println (float.toString () + "f"); }}

Dengan kemudian mengimplementasikan VisitableFloatkelas dan VisitableCollectionkelas yang masing-masing memanggil metode pengunjung yang sesuai, Anda mendapatkan hasil yang sama seperti metode if-else yang berantakan messyPrintCollectiontetapi dengan pendekatan yang jauh lebih bersih. Dalam visitCollection(), Anda memanggil Visitable.accept(this), yang pada gilirannya memanggil metode pengunjung yang benar. Itu disebut pengiriman ganda; yang Visitormemanggil metode dalam Visitablekelas, yang menyerukan kembali ke Visitorkelas.

Meskipun Anda telah membersihkan pernyataan if-else dengan menerapkan pengunjung, Anda masih memperkenalkan banyak kode tambahan. Anda harus membungkus objek asli Anda, Stringdan Float, dalam objek yang mengimplementasikan Visitableantarmuka. Meskipun menjengkelkan, hal itu biasanya tidak menjadi masalah karena koleksi yang biasanya Anda kunjungi dapat dibuat hanya berisi objek yang mengimplementasikan Visitableantarmuka.

Tetap saja, sepertinya masih banyak pekerjaan ekstra. Lebih buruk lagi, apa yang terjadi jika Anda menambahkan Visitabletipe baru , misalnya VisitableInteger? Itulah salah satu kelemahan utama dari pola Pengunjung. Jika Anda ingin menambahkan Visitableobjek baru , Anda harus mengubah Visitorantarmuka dan kemudian mengimplementasikan metode itu di setiap Visitorkelas implementasi Anda . Anda dapat menggunakan kelas dasar abstrak Visitordengan fungsi tanpa operasi default sebagai ganti antarmuka. Itu akan mirip dengan Adapterkelas di GUI Java. Masalah dengan pendekatan itu adalah Anda perlu menggunakan warisan tunggal Anda, yang sering kali ingin Anda simpan untuk hal lain, seperti perpanjangan StringWriter. Itu juga akan membatasi Anda untuk hanya dapat mengunjungi Visitableobjek dengan sukses.

Untungnya, Java memungkinkan Anda membuat pola Pengunjung jauh lebih fleksibel sehingga Anda dapat menambahkan Visitableobjek sesuka hati. Bagaimana? Jawabannya adalah dengan menggunakan refleksi. Dengan a ReflectiveVisitor, Anda hanya memerlukan satu metode di antarmuka Anda:

antarmuka publik ReflectiveVisitor {kunjungan kekosongan publik (Objek o); }

Oke, itu cukup mudah. Visitablebisa tetap sama, dan saya akan membahasnya sebentar lagi. Untuk saat ini, saya akan menerapkan PrintVisitormenggunakan refleksi:

public class PrintVisitor mengimplementasikan ReflectiveVisitor {public void visitCollection (Collection collection) {... sama seperti di atas ...} public void visitString (String string) {... sama seperti di atas ...} public void visitFloat (Float float) { ... sama seperti di atas ...} public void default (Object o) {System.out.println (o.toString ()); } public void visit (Object o) {// Class.getName () juga mengembalikan informasi paket. // Ini menghapus informasi paket yang memberi kita // hanya nama kelas String methodName = o.getClass (). GetName (); methodName = "kunjungi" + methodName.substring (methodName.lastIndexOf ('.') + 1); // Sekarang kita mencoba untuk memanggil metode kunjungi coba {// Dapatkan metode visitFoo (Foo foo) Metode m = getClass (). GetMethod (methodName, new Class [] {o.getClass ()}); // Coba aktifkan visitFoo (Foo foo) m.invoke (ini, Objek baru [] {o});} catch (NoSuchMethodException e) {// Tidak ada metode, jadi lakukan implementasi default default (o); }}}

Sekarang Anda tidak membutuhkan Visitablekelas pembungkus. Anda tinggal menelepon visit(), dan itu akan dikirim ke metode yang benar. Salah satu aspek yang bagus adalah yang visit()dapat dikirim sesuka hati. Itu tidak harus menggunakan refleksi - itu bisa menggunakan mekanisme yang sama sekali berbeda.

Dengan baru PrintVisitor, Anda memiliki metode untuk Collections, Stringsdan Floats, tapi kemudian Anda menangkap semua jenis tertangani dalam menangkap pernyataan. Anda akan mengembangkan visit()metode ini sehingga Anda dapat mencoba semua kelas super juga. Pertama, Anda akan menambahkan metode baru yang disebut getMethod(Class c)yang akan mengembalikan metode ke pemanggilan, yang mencari metode yang cocok untuk semua kelas super Class cdan kemudian semua antarmuka untuk Class c.

Metode dilindungi getMethod (Kelas c) {Kelas newc = c; Metode m = null; // Coba kelas super sementara (m == null && newc! = Object.class) {String method = newc.getName (); method = "visit" + method.substring (method.lastIndexOf ('.') + 1); coba {m = getClass (). getMethod (metode, Kelas baru [] {newc}); } catch (NoSuchMethodException e) {newc = newc.getSuperclass (); }} // Mencoba antarmuka. Jika perlu, Anda // dapat mengurutkannya terlebih dahulu untuk menentukan antarmuka 'yang dapat dikunjungi' menang // jika sebuah objek mengimplementasikan lebih dari satu. if (newc == Object.class) {Class [] interfaces = c.getInterfaces (); untuk (int i = 0; i <interfaces.length; i ++) {String method = interfaces [i] .getName (); method = "visit" + method.substring (method.lastIndexOf ('.') + 1); coba {m = getClass (). getMethod (metode, Kelas baru [] {antarmuka [i]});} menangkap (NoSuchMethodException e) {}}} if (m == null) {coba {m = thisclass.getMethod ("visitObject", Kelas baru [] {Object.class}); } menangkap (Pengecualian e) {// Tidak bisa terjadi}} return m; }

Ini terlihat rumit, tetapi sebenarnya tidak. Pada dasarnya, Anda hanya mencari metode berdasarkan nama kelas yang Anda lewati. Jika Anda tidak menemukannya, Anda mencoba superclass-nya. Kemudian jika Anda tidak menemukannya, coba antarmuka apa pun. Terakhir, Anda dapat mencoba visitObject()sebagai default.

Perhatikan bahwa demi mereka yang terbiasa dengan pola Pengunjung tradisional, saya telah mengikuti konvensi penamaan yang sama untuk nama metode. Namun, karena beberapa dari Anda mungkin telah memperhatikan, akan lebih efisien untuk menamai semua metode "kunjungan" dan biarkan jenis parameter menjadi pembeda. Namun, jika Anda melakukannya, pastikan Anda mengubah visit(Object o)nama metode utama menjadi seperti dispatch(Object o). Jika tidak, Anda tidak akan memiliki metode default untuk digunakan kembali, dan Anda perlu mentransmisikan ke Objectsetiap kali Anda memanggil visit(Object o)untuk memastikan pola pemanggilan metode yang benar diikuti.

Sekarang, Anda memodifikasi visit()metode untuk memanfaatkan getMethod():

kunjungan publik void (Objek objek) {coba {Metode metode = getMethod (getClass (), object.getClass ()); method.invoke (ini, Object baru [] {object}); } menangkap (Pengecualian e) {}}

Sekarang, objek pengunjung Anda jauh lebih kuat. Anda dapat mengirimkan objek arbitrer apa pun dan memiliki beberapa metode yang menggunakannya. Plus, Anda mendapatkan keuntungan tambahan karena memiliki metode default visitObject(Object o)yang dapat menangkap apa pun yang tidak Anda tentukan. Dengan sedikit lebih banyak pekerjaan, Anda bahkan dapat menambahkan metode untuk visitNull().

Saya telah menyimpan Visitableantarmuka di sana karena suatu alasan. Manfaat sisi lain dari pola Pengunjung tradisional adalah memungkinkan Visitableobjek untuk mengontrol navigasi dari struktur objek. Misalnya, jika Anda memiliki TreeNodeobjek yang diimplementasikan Visitable, Anda dapat memiliki accept()metode yang melintasi ke node kiri dan kanannya:

public void accept (Pengunjung pengunjung) {visitor.visitTreeNode (this); visitor.visitTreeNode (leftsubtree); visitor.visitTreeNode (rightsubtree); }

Jadi, hanya dengan satu modifikasi lagi pada Visitorkelas, Anda dapat mengizinkan Visitablenavigasi terkontrol:

public void visit(Object object) throws Exception { Method method = getMethod(getClass(), object.getClass()); method.invoke(this, new Object[] {object}); if (object instanceof Visitable) { callAccept((Visitable) object); } } public void callAccept(Visitable visitable) { visitable.accept(this); } 

If you've implemented a Visitable object structure, you can keep the callAccept() method as is and use Visitable-controlled navigation. If you want to navigate the structure within the visitor, you just override the callAccept() method to do nothing.

The power of the Visitor pattern comes into play when using several different visitors across the same collection of objects. For example, I have an interpreter, an infix writer, a postfix writer, an XML writer, and a SQL writer working across the same collection of objects. I could easily write a prefix writer or a SOAP writer for the same collection of objects. In addition, those writers can gracefully work with objects they don't know about or, if I choose, they can throw an exception.

Conclusion

By using Java reflection, you can enhance the Visitor design pattern to provide a powerful way to operate on object structures, giving the flexibility to add new

Visitable

types as needed. I hope you are able to use that pattern somewhere in your coding travels.

Jeremy Blosser telah memprogram di Java selama lima tahun, selama itu dia bekerja untuk berbagai perusahaan perangkat lunak. Dia sekarang bekerja untuk perusahaan startup, Software Instruments. Anda dapat mengunjungi Situs web Jeremy di //www.blosser.org.

Pelajari lebih lanjut tentang topik ini

  • Beranda Pola

    //www.hillside.net/patterns/

  • Pola Desain Elemen Perangkat Lunak Berorientasi Objek yang Dapat Digunakan Kembali, Erich Gamma, et al. (Addison-Wesley, 1995)

    //www.amazon.com/exec/obidos/ASIN/0201633612/o/qid=963253562/sr=2-1/002-9334573-2800059

  • Pola di Jawa, Volume 1, Mark Grand (John Wiley & Sons, 1998)

    //www.amazon.com/exec/obidos/ASIN/0471258393/o/qid=962224460/sr=2-1/104-2583450-5558345

  • Pola di Jawa, Volume 2, Mark Grand (John Wiley & Sons, 1999)

    //www.amazon.com/exec/obidos/ASIN/0471258415/qid=962224460/sr=1-4/104-2583450-5558345

  • Lihat semua Tips Java sebelumnya dan kirimkan Tips Anda sendiri

    //www.javaworld.com/javatips/jw-javatips.index.html

Artikel ini, "Tip Java 98: Merefleksikan pola desain Pengunjung" ini awalnya diterbitkan oleh JavaWorld.