Java實現在訪問者模式中使用反射

文學敏發表於2016-08-03

集合型別在物件導向程式設計中很常用,這也帶來一些程式碼相關的問題。比如,“怎麼操作集合中不同型別的物件?”

一種做法就是遍歷集合中的每個元素,然後根據它的型別而做具體的操作。這會很複雜,尤其當你不知道集合中元素的型別時。如果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

瞭解更多

相關文章