Java實現在訪問者模式中使用反射
集合型別在物件導向程式設計中很常用,這也帶來一些程式碼相關的問題。比如,“怎麼操作集合中不同型別的物件?”
一種做法就是遍歷集合中的每個元素,然後根據它的型別而做具體的操作。這會很複雜,尤其當你不知道集合中元素的型別時。如果y要列印集合中的元素,可以寫一個這樣的方法:
public void messyPrintCollection(Collection collection) { Iterator iterator = collection.iterator() while (iterator.hasNext()) System.out.println(iterator.next().toString()) }
看起來很簡單。僅僅呼叫了Object.toString()方法並列印出了物件,對吧?但如果你的集合是一個包含hashtable的vector呢?那會變得更復雜。你必須檢查集合返回物件的型別:
public void messyPrintCollection(Collection collection) { Iterator iterator = collection.iterator() while (iterator.hasNext()) { Object o = iterator.next(); if (o instanceof Collection) messyPrintCollection((Collection)o); else System.out.println(o.toString()); } }
好了,現在可以處理內嵌的集合物件,但其他物件返回的字串不是你想要的呢?假如你想在字串物件加上引號,想在Float物件後加一個f,你該怎麼做?程式碼會變得更加複雜:
public void messyPrintCollection(Collection collection) { Iterator iterator = collection.iterator() while (iterator.hasNext()) { Object o = iterator.next(); if (o instanceof Collection) messyPrintCollection((Collection)o); else if (o instanceof String) System.out.println("'"+o.toString()+"'"); else if (o instanceof Float) System.out.println(o.toString()+"f"); else System.out.println(o.toString()); } }
程式碼很快就變雜亂了。你不想讓程式碼中包含一大堆的if-else語句!怎麼避免呢?訪問者模式可以幫助你。
為實現訪問者模式,你需要建立一個Visitor介面,為被訪問的集合物件建立一個Visitable介面。接下來需要建立具體的類來實現Visitor和Visitable介面。這兩個介面大致如下:
public interface Visitor { public void visitCollection(Collection collection); public void visitString(String string); public void visitFloat(Float float); } public interface Visitable { public void accept(Visitor visitor); }
對於一個具體的String類,可以這麼實現:
public class VisitableString implements Visitable { private String value; public VisitableString(String string) { value = string; } public void accept(Visitor visitor) { visitor.visitString(this); } }
在accept方法中,根據不同的型別,呼叫visitor中對應的方法:
visitor.visitString(this)
具體Visitor的實現方式如下:
public class PrintVisitor implements Visitor { 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"); } }
到時候,只要實現了VisitableFloat類和VisitableCollection類並呼叫合適的visitor方法,你就可以去掉包含一堆if-else結構的messyPrintCollection方法,採用一種十分清爽的方式實現了同樣的功能。visitCollection()方法呼叫了Visitable.accept(this),而accept()方法又反過來呼叫了visitor中正確的方法。這就是雙分派:Visitor呼叫了一個Visitable類中的方法,這個方法又反過來呼叫了Visitor類中的方法。
儘管實現visitor後,if-else語句不見了,但還是引入了很多附加的程式碼。你不得不將原始的物件——String和Float,打包到一個實現Visitable介面的類中。雖然很煩人,但這一般來說不是個問題。因為你可以限制被訪問集合只能包含Visitable物件。
然而,這還有很多附加的工作要做。更壞的是,當你想增加一個新的Visitable型別時怎麼辦,比如VisitableInteger?這是訪問者模式的一個主要缺點。如果你想增加一個新的Visitable型別,你不得不改變Visitor介面以及每個實現Visitor介面方法的類。你可以不把Visitor設計為介面,取而代之,可以把Visitor設計為一個帶有空操作的抽象基類。這與Java GUI中的Adapter類很相似。這麼做的問題是你會用盡單次繼承,而常見的情形是你還想用繼承實現其他功能,比如繼承StringWriter類。這同樣只能成功訪問實現Visitable介面的物件。
幸運的是,Java可以讓你的訪問者模式更靈活,你可以按你的意願增加Visitable物件。怎麼實現呢?答案是使用反射。使用反射的ReflectiveVisitor介面只需要一個方法:
public interface ReflectiveVisitor { public void visit(Object o); }
好了,上面很簡單。Visitable介面先不動,待會我會說。現在,我使用反射實現PrintVisitor類。
public class PrintVisitor implements ReflectiveVisitor { public void visitCollection(Collection collection) { ... same as above ... } public void visitString(String string) { ... same as above ... } public void visitFloat(Float float) { ... same as above ... } public void default(Object o) { System.out.println(o.toString()); } public void visit(Object o) { // Class.getName() returns package information as well. // This strips off the package information giving us // just the class name String methodName = o.getClass().getName(); methodName = "visit"+ methodName.substring(methodName.lastIndexOf('.')+1); // Now we try to invoke the method visit<methodName> try { // Get the method visitFoo(Foo foo) Method m = getClass().getMethod(methodName, new Class[] { o.getClass() }); // Try to invoke visitFoo(Foo foo) m.invoke(this, new Object[] { o }); } catch (NoSuchMethodException e) { // No method, so do the default implementation default(o); } } }
現在你無需使用Visitable包裝類(包裝了原始型別String、Float)。你可以直接訪問visit(),它會呼叫正確的方法。visit()的一個優點是它會分派它認為合適的方法。這不一定使用反射,可以使用完全不同的一種機制。
在新的PrintVisitor類中,有對應於Collections、String和Float的操作方法;對於不能處理的型別,可以通過catch語句捕捉。對於不能處理的型別,可以通過擴充套件visit()方法來嘗試處理它們的所有超類。首先,增加一個新的方法getMethod(Class c),返回值是一個可被觸發的方法。它會搜尋Class c的所有父類和介面,以找到一個匹配方法。
protected Method getMethod(Class c) { Class newc = c; Method m = null; // Try the superclasses while (m == null && newc != Object.class) { String method = newc.getName(); method = "visit" + method.substring(method.lastIndexOf('.') + 1); try { m = getClass().getMethod(method, new Class[] {newc}); } catch (NoSuchMethodException e) { newc = newc.getSuperclass(); } } // Try the interfaces. If necessary, you // can sort them first to define 'visitable' interface wins // in case an object implements more than one. if (newc == Object.class) { Class[] interfaces = c.getInterfaces(); for (int i = 0; i < interfaces.length; i++) { String method = interfaces[i].getName(); method = "visit" + method.substring(method.lastIndexOf('.') + 1); try { m = getClass().getMethod(method, new Class[] {interfaces[i]}); } catch (NoSuchMethodException e) {} } } if (m == null) { try { m = thisclass.getMethod("visitObject", new Class[] {Object.class}); } catch (Exception e) { // Can't happen } } return m; }
這看上去很複雜,實際上並不。大致來說,首先根據傳入的class名稱搜尋可用方法;如果沒找到,就嘗試從父類搜尋;如果還沒找到,就從介面中嘗試。最後,(仍沒找到)可以使用visitObject()作為預設方法。
由於大家對傳統的訪問者模式比較熟悉,這裡沿用了之前方法命名的慣例。但是,有些人可能注意到,把所有的方法都命名為“visit”並通過引數型別不同來區分,這樣更高效。然而,如果你這麼做,你必須把visit(Object o)方法的名稱改為其他,比如dispatch(Object o)。否則,(當沒有對應處理方法時),你無法退回到預設的處理方法,並且當你呼叫visit(Object o)方法時,為了確保正確的方法呼叫,你必須將引數強制轉化為Object。
為了利用getMethod()方法,現在需要修改一下visit()方法。
public void visit(Object object) { try { Method method = getMethod(getClass(), object.getClass()); method.invoke(this, new Object[] {object}); } catch (Exception e) { } }
現在,visitor類更加強大了——可以傳入任意的物件並且有對應的處理方法。另外,有一個預設處理方法,visitObject(Object o),的好處就是就可以捕捉到任何沒有明確說明的型別。再稍微修改下,你甚至可以新增一個visitNull()方法。
我仍保留Visitable介面是有原因的。傳統訪問者模式的另一個好處是它可以通過Visitable物件控制物件結構的遍歷順序。舉例來說,假如有一個實現了Visitable介面的類TreeNode,它在accept()方法中遍歷自己的左右節點。
public void accept(Visitor visitor) { visitor.visitTreeNode(this); visitor.visitTreeNode(leftsubtree); visitor.visitTreeNode(rightsubtree); }
這樣,只要修改下Visitor類,就可以通過Visitable類控制遍歷:
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); }
如果你實現了Visitable物件的結構,你可以保持callAccept()不變,就可以使用Visitable控制的物件遍歷。如果你想在visitor中遍歷物件結構,你只需重寫allAccept()方法,讓它什麼都不做。
當使用幾個不同的visitor去操作同一個物件集合時,訪問者模式的力量就會展現出來。比如,當前有一個直譯器、中序遍歷器、後續遍歷器、XML編寫器以及SQL編寫器,它們可以處理同一個物件集合。我可以輕鬆地為這個集合再寫一個先序遍歷器或者一個SOAP編寫器。另外,它們可以很好地相容它們不識別的型別,或者我願意的話可以讓它們丟擲異常。
總結
使用Java反射,可以使訪問者模式提供一種更加強大的方式操作物件結構,可以按照需求靈活地增加新的Visitable
型別。我希望在你的程式設計之旅中可以使用訪問者模式。
Jeremy Blosser有5年的Java程式設計經驗,他在很多軟體公司工作過。他現在在一家創業型公司Software Instruments供職。你可以訪問Jeremy的網站http://www.blosser.org
瞭解更多
- Patterns homepage
- [Design PatternsElements of Reusable Object-Oriented Software, Erich Gamma, et al. (Addison-Wesley, 1995)](http://www.amazon.com/exec/obidos/ASIN/0201633612/o/qid=963253562/sr=2-1/002-9334573-2800059)
- Patterns in Java, Volume 1, Mark Grand (John Wiley & Sons, 1998)
- Patterns in Java, Volume 2, Mark Grand (John Wiley & Sons, 1999)
- View all previous Java Tips and submit your own
相關文章
- 設計模式學習-使用go實現訪問者模式設計模式Go
- 訪問者模式模式
- 15.java設計模式之訪問者模式Java設計模式
- 訪問使用者中心實現認證
- 設計模式 - ASM 中的訪問者模式設計模式ASM
- 中介者模式及在NetCore中的使用MediatR來實現模式NetCore
- Java反射全解析(使用、原理、問題、在Android中的應用)Java反射Android
- 行為模式-訪問者模式模式
- Java進階篇設計模式之十 ---- 訪問者模式和中介者模式Java設計模式
- 設計模式學習筆記(二十一)訪問者模式及其實現設計模式筆記
- python-訪問者模式Python模式
- 設計模式(十六)——訪問者模式設計模式
- Java如何實現延時訪問Java
- 極簡設計模式-訪問者模式設計模式
- 深入淺出訪問者模式模式
- DesignPattern_訪問者模式_19模式
- tour cpp: std::variant 實現無繼承層次的訪問者模式繼承模式
- MVC模式在Java Web應用程式中的實現MVC模式JavaWeb
- 設計模式學習之訪問者模式設計模式
- C#設計模式之訪問者模式C#設計模式
- 【趣味設計模式系列】之【訪問者模式】設計模式
- 聊聊OOP中的設計原則以及訪問者模式OOP模式
- 使用函式式實現觀察者模式模式函式模式
- 設計模式(二十三)訪問者設計模式
- 在SSRS 2008實現匿名訪問報表PD
- Java通過SSLEngine與NIO實現HTTPS訪問JavaHTTP
- 使用lambda實現裝飾者模式 - Voxxed模式
- java實現生產者消費者問題Java
- 在Java中,使用HttpUtils實現傳送HTTP請求JavaHTTP
- 設計模式學習-使用go實現建造者模式設計模式Go
- 使用BlockQueue實現生產者和消費者模式BloC模式
- Android理解設計模式之組合模式、迭代器模式、訪問者模式Android設計模式
- 設計模式:單例模式的使用和實現(JAVA)設計模式單例Java
- 「補課」進行時:設計模式(18)——訪問者模式設計模式
- Java 8中實現構建器模式Java模式
- 設計模式學習-使用go實現觀察者模式設計模式Go
- 02 使用配置檔案+反射實現反射
- 設計模式:建造者模式及在jdk中的體現,建造者模式和工廠模式區別設計模式JDK
- JAVA-反射與工廠模式Java反射模式