熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java高級技術 >> 正文

在組合模式中實現訪問者(Visitor)模式

2022-06-13   來源: Java高級技術 

  本文從一個給定的實現了組合(Composite)模式的例子開始說明怎麼在這個數據結構上實現業務邏輯代碼依次介紹了非面向對象的方式在組合結構中加入方法使用訪問者(Visitor)模式以及用改進後的訪問者(Visitor)模式來實現相同的業務邏輯代碼並且對於每種實現分別給出了優缺點

  讀者定位於具有Java程序開發和設計模式經驗的開發人員

  讀者通過本文可以學到如何在組合(Composite)模式中實現各種不同的業務方法及其優缺點

  組合(Composite)模式

  組合模式是結構型模式中的一種GOF的《設計模式》一書中對使用組合模式的意圖描述如下將對象組合成樹形結構以表示部分整體的層次結構Composite使得用戶對單個對象和組合對象的使用具有一致性

  組合模式應用廣泛根據GOF中對組合模式的定義Composite模式一般由Component接口Leaf類和Composite類組成現在需要對一個軟件產品管理系統的實體建模某公司開發了一系列軟件集(SoftwareSet)包含了多種品牌(Brand)的軟件產品就象IBM提供了LotusWebsPhere等品牌每個品牌下面又有各種產品(Product)如IBM的Lotus下面有Domino Server/Client產品等建模後的類圖如下(代碼可以參見隨本文帶的附件中包comtestentity下所有的源文件)

  

  如圖所示

  ()接口SoftwareComponent就是對應於組合模式中的Component接口它定義了所有類共有接口的缺省行為

  ()AbsSoftwareComposite類對應於Composite類並且是抽象類所有可以包含子節點的類都擴展這個類這個類的主要功能是用來存儲子部件實現了接口中的方法部分可以重用的代碼寫在此類中

  ()SoftwareSet類繼承於AbsSoftwareComposite類對應於軟件集軟件集下直接可以包含品牌(Brand)也可以直接包含不屬於任何品牌的產品(Product)

  ()Brand類繼承於AbsSoftwareComposite類對應於品牌包含了品牌名屬性並且用來存儲Product類的實例

  ()Product類就是對應的Leaf類表示葉子節點葉子節點沒有子節點

  用不同的方法實現業務邏輯

  數據結構建立好之後需要在這個數據結構上添加方法實現業務邏輯比如現在的這個例子中有這樣的需求給定一些用戶選擇好的產品需要計算出這些選中後軟件的總價格下面開始介紹如何使用各種不同的方法來實現這個業務邏輯

  非面向對象的編程方式

  這種方式下編程思路最簡單遍歷SoftwareSet實例中的所有節點如果遍歷到的當前對象是Product的話就累加否則繼續遍歷下一層直到全部遍歷完畢代碼片斷如下



    /**
     * 取得某個SoftwareComponent對象下面所有Product的價格
     * @param brand
     * @return
     */
    public double getTotalPrice(SoftwareComponent softwareComponent) {
        SoftwareComponent temp = softwareComponent;
        double totalPrice = ;
        //如果傳入的實例是SoftwareSet的類型
        if (temp instanceof SoftwareSet) {
            Iterator it = ((SoftwareSet) softwareComponent)getChilds()
                   &erator();
            while (ithasNext()) {//遍歷
                temp = (SoftwareComponent) itnext();
                //如果子對象是Product類型的直接累加
                if (temp instanceof Product) {
                    Product product = (Product) temp;
                    totalPrice += productgetPrice();
                } else if (temp instanceof Brand) { 
                //如果子對象是Brand類型的則遍歷Brand下面所有的產品並累加
                    Brand brand = (Brand) temp;
                    totalPrice += getBrandPrice(brand);
                }
            }
        } else if (temp instanceof Brand) {
            //如果傳入的實例是SoftwareSet的類型則遍歷Brand下面所有的產品並累加
            totalPrice += getBrandPrice((Brand) temp);
        } else if (temp instanceof Product) {
            //如果子對象是Product類型的直接返回價格
            return ((Product) temp)getPrice();
        }
        return totalPrice;
    }

    /**
     * 取得某個Brand對象下面所有Product的價格
     * @param brand
     * @return
     */
    private double getBrandPrice(Brand brand) {
        Iterator brandIt = brandgetChilds(erator();
        double totalPrice = ;
        while (brandIthasNext()) {
            Product product = (Product) brandItnext();
            totalPrice += productgetPrice();
        }
        return totalPrice;
    }

  這段代碼的好處是實現業務邏輯的時候無需對前面已經定好的數據結構做改動並且效率比較高缺點是代碼凌亂而且頻繁使用了instanceof判斷類型和強制類型轉換代碼的可讀性不強如果層次多了代碼就更加混亂

  面向對象的編程方式(將計算價格的方法加入數據結構中)

  下面我們采用面向對象的方式可以這麼做在接口SoftWareComponent中加入一個方法名叫getTotalPrice方法的聲明如下



    /**
     * 返回該節點中所有子節點對象的價格之和
     * @return
     */
    public double getTotalPrice();
  由於類Brand和SoftwareSet都繼承了AbsSoftwareComposite我們只需在類AbsSoftwareComposite中實現該方法getTotalPrice方法即可如下


    public double getTotalPrice() {
        Iterator it = erator();
        double price = ;
        while (ithasNext()) {
            SoftwareComponent softwareComponent = (SoftwareComponent) itnext();
                           //自動遞歸調用各個對象的getTotalPrice方法並累加
            price += softwareComponentgetTotalPrice();
        }
        return price;
    }
  在Product類中實現如下


    public double getTotalPrice(){
        return price;
    }
  在外面需要取得某個對象的總價格的時候只需這樣寫(在本文的例子comtestbusinessSoftwareManager中可以找到這段代碼)



    // getMockData()方法返回數據
    SoftwareComponent data = getMockData();
    //只需直接調用data對象的getTotalPrice 方法就可以返回該對象下所有product對象的價格
    double price = data getTotalPrice();
    //找到某個對象後直接調用其getTotalPrice方法也可以返回總價格
    price = data findSoftwareComponentByID(id)getTotalPrice();

  現在把業務邏輯的實現都放在了數據結構中(組合模式的結構中)好處很明顯每個類只管理自己相關的業務代碼的實現跟前面舉的面向過程方式的實現方式相比沒有了instanceof和強制類型轉換但是不好的地方是如果需要增加新的業務方法的話就很麻煩必須在接口SoftWareComponent中首先聲明該方法然後在各個子類中實現並且重新編譯

  使用訪問者模式

  使用訪問者模式就能解決上面提到的問題如果要經常增加或者刪除業務功能方法的話需要頻繁地對程序進行重新實現和編譯根據面向對象設計原則之一的SRP(單一職責原則)原則如果一個類承擔了多於一個的職責那麼引起該類變化的原因就會有多個就會導致脆弱的設計在發生變化時原有的設計可能會遭到意想不到的破壞下面我們引入了一個叫做Visitor的接口該接口中定義了針對各個子類的訪問方法如下所示



    public interface Visitor {
        public void visitBrand(Brand brand);
        public void visitSoftwareSet(SoftwareSet softwareSet);
        public void visitProduct(Product product);
    }
  visitBrand方法是訪問Brand對象節點的時候用的剩下的方法依次類推並在接口SoftwareComponent中增加一個方法


    public void accept(Visitor visitor);
  在SoftwareSet中實現接口中的accept方法首先直接調用Visitor接口中的visitSoftwareSet方法傳入的參數是本身對象然後遞歸調用子對象的accept方法



    public void accept(Visitor visitor) {
        visitorvisitSoftwareSet(this);
        Iterator it = erator();
        while (ithasNext()) {
            SoftwareComponent component = (SoftwareComponent)itnext();
            componentaccept(visitor);
        }
    }
   在Brand中實現接口中的accept方法首先直接調用Visitor接口中的visitBrand方法傳入的參數是本身對象然後遞歸調用子對象的accept方法


    public void accept(Visitor visitor) {
        visitorvisitBrand(this);
        Iterator it = erator();
        while (ithasNext()) {
            SoftwareComponent component = (SoftwareComponent)itnext();
            componentaccept(visitor);
        }
    }
   其實在上面的兩個類的實現中可以將遍歷子節點並調用其accept方法的代碼寫到父類AbsSoftwareComposite中的某個方法中然後直接調用父類中的這個方法即可這裡為了解釋方便分別寫在了兩個子類中
  
  在Product中實現接口中的accept方法直接調用Visitor接口的visitProduct方法即可



    public void accept(Visitor visitor) {
        visitorvisitProduct(this);
    }
   下面需要實現Visitor接口類名是CaculateTotalPriceVisitor實現了計算總價格的業務邏輯實現代碼如下所示



    public class CaculateTotalPriceVisitor implements Visitor {
        private double totalPrice;    
        public void visitBrand(Brand brand) {
        }
        public void visitSoftwareSet(SoftwareSet softwareSet) {
        }
        public void visitProduct(Product product) {
            //每次在組合的結構中碰到Product對象節點的時候就會調用此方法
            totalPrice += productgetPrice();
        }
        public double getTotalPrice() {
            return totalPrice;
        }
    }
   上面那段代碼中首先在類內定義一個總價格的屬性由於Brand和SoftwareSet都沒有價格因此在實現中只需在visitProduct方法中累加totalPrice即可在外面如果需要計算總價格的話這樣寫(在本文的例子comtestbusinessSoftwareManager中可以找到這段代碼)



    //建立一個新的Visitor對象
    CaculateTotalPriceVisitor visitor = new CaculateTotalPriceVisitor();
    //將該visitor對象傳到結構中
    dataaccept(visitor);
    //調用visitor對象的getTotalPrice()方法就返回了總價格
    double price = visitorgetTotalPrice();

  下面是它的時序圖在類SoftwareManager中的main方法中調用軟件集對象(data)的accept方法並將生成的visitor對象傳給它accept方法開始遞歸調用各個子對象的accept方法如果當前的對象是SoftwareSet的實例則調用visitor對象visitSoftwareSet方法在visitor對象中對該節點的數據進行一些處理然後返回依次類推遍歷到Brand對象和Product對象也與此類似當前的邏輯是計算軟件產品的總價格因此當遍歷到Product對象的時候取出產品的價格並且累加最後當結構遍歷完畢後調用visitor對象的getTotalPrice方法返回給定軟件集對象的(data)的總的價格如果需要加入一個新的計算邏輯只實現Visitor接口並且將該類的實例傳給data對象的accept方法就可以實現不同的邏輯方法了

  

  點擊小圖看大圖

  我們可以看到通過訪問者模式很好地解決了如何加入新的業務代碼而無需重新改動編譯既有代碼但是該模式也不是沒有缺點如果在組合模式中結構加入新的子類的話會導致接口Visitor也跟著改動導致所有Visitor的子類都需要實現新增的方法因此這種訪問者模式適合於結構不經常變動的情況

  改進訪問者模式

  前面我們說到了如何使用Visitor模式及使用該模式後的優缺點下面舉具體的例子說明假設現在客戶提出了一個產品集(ProductSet)的概念隨著公司軟件版本的增多需要將同一個版本的產品(Product)都放到產品集(ProductSet)中而一個品牌包含有多個產品集因為現在組合結構中增加了一個節點所以在Visitor接口中也必須隨之增加一個叫做visitProductSet的方法並且會導致原有系統中所有已經實現了Visitor接口的類都需要重新實現並編譯用Java的反射機制可以解決這個問題

  使用Java的Method Reflection機制實現訪問者模式

  首先我們需要改變一下Visitor接口接口名叫做ReflectionVisitor如下所示



    public interface ReflectionVisitor {
        /**
         * 定義了一個訪問節點的方法
         * @param softwareComposite
         */
        public void visitSoftwareComposite(Object softwareComposite);
    }

  在現在的接口的方法裡能接受任意的對象(參數是Object)

  下面實現接口ReflectionVisitor名叫ReflectionVisitorImpl代碼如下所示



    public class ReflectionVisitorImpl implements ReflectionVisitor {
        public void visitSoftwareComposite(Object softwareComposite) {
            //判斷是否是null
            if (softwareComposite == null) {
                throw new NullPointerException(The visit node should not be null!);
            }
            //組裝class數組即調用動態方法的時候參數的類型
            Class[] classes = new Class[] { softwareCompositegetClass() };
            //組裝與class數組相對應的值
            Object[] objects = new Object[] { softwareComposite };
            try {
                //查找visit方法
                Method m = getClass()getMethod(visit classes);
                //調用該方法
                minvoke(this objects);
            } catch (NoSuchMethodException e) {
                //沒有找到相應的方法
                Systemout
                        println(You did not implement the visit method for class:
                                + softwareCompositegetClass());
            } catch (Exception e) {
                //發生了別的異常
                Systemoutprintln(Catched excepction in visit method);
                eprintStackTrace();
            }
        }
    }

  這段代碼首先判斷傳入的對象是否是空指針然後創建class數組和object數組然後用getMethod方法取得方法名是visit方法的參數是對象softwareComposite對應的類的方法最後調用該方法調用該方法的時候可能會發生NoSuchMethodException異常發生這個異常就表明它的子類或者當前類中沒有與參數中傳入相對應的visit方法

  下面再來寫新版本Visitor類擴展剛寫好的那個ReflectionVisitorImpl類名叫CaculateTotalPriceReflectionVisitor如下所示



    public class CaculateTotalPriceReflectionVisitor extends ReflectionVisitorImpl {
        private double totalPrice;
        public void visit(Product product) {
            totalPrice += productgetPrice();
        }
        public void visit(SoftwareSet softwareSet) {
            Systemoutprintln(No price for software set);
        }
        public double getTotalPrice() {
            return totalPrice;
        }
    }
   代碼中聲明了兩個visit方法(因為在類ReflectionVisitorImpl中查找名為visit參數與傳進去的對象匹配的的方法)一個是給Product的另外一個是給SoftwareSet的在這裡SoftwareSet中並沒有價格只需當前的對象是類Product的實例的時候將價格累加即可如果在組合模式的結構中增加了新的類只需要在ReflectionVisitorImpl的擴展類中聲明一個visit方法該方法的參數是新增加的類對於文中的例子只需增加下面的一個方法


    public void visit(ProductSet productSet) {
        //實現的代碼
    }

  在組合結構的接口SoftwareComponent中改一下accept方法參數是修改後的Visitor接口如下所示

  public void accept(ReflectionVisitor visitor);

  由於在類SoftwareSetBrand和ProductSet中實現上面accept方法的代碼都一樣因此把代碼抽象到上層共有的抽象類AbsSoftwareComposite中如下所示



    public void accept(ReflectionVisitor visitor) {
        visitorvisitSoftwareComposite(this);
        Iterator it = erator();
        while (ithasNext()) {
            SoftwareComponent component = (SoftwareComponent) itnext();
            //遞歸調用子對象的accept方法
            componentaccept(visitor);
        }
    }
   現在如果想在外面要調用的話代碼如下所示(在本文的例子comtestbusinessSoftwareManager中可以找到這段代碼)



    //建立一個新的Visitor對象
    CaculateTotalPriceReflectionVisitor reflectionVisitor 
        = new CaculateTotalPriceReflectionVisitor();
    //將該visitor對象傳到結構中
    dataaccept(reflectionVisitor);
    //調用visitor對象的getTotalPrice()方法就返回了總價格
    double price = reflectionVisitorgetTotalPrice();

  另外由於沒有實現Brand類的visit方法在組合結構遍歷到Brand的節點的時候會拋出NoSuchMethodException異常就是沒有關於該節點方法的實現在當前的程序中會打印出一句話

  You did not implement the visit method for class:class comtestentityBrand

  如果運行程序時發生了別的異常請參見相應的Java API文檔

  在現在的改進後的訪問者模式中如果在組合的結構中新增或刪除節點並不會對已經實現了的Visitor產生任何影響如果新增了業務方法只需擴展類ReflectionVisitorImpl就可以了因此很好地解決了訪問者模式的問題

  改進訪問者模式實現與既有代碼對接

  到現在為止改進後的訪問者模式好像已經很好地解決了所有出現的問題但是考慮到有下面的這種情況現在需要寫一個JSP的標簽庫(TagLib)這個標簽庫還必須具有Visitor的功能(就是需要有遍歷節點的功能)可以將節點的內容根據需要打印到HTML頁面中由於標簽本身需要繼承相應的類(如TagSupport)如果繼續使用上面提供的方法將無法實現因為Java不允許多重繼承不過我們可以將原有ReflectionVisitorImpl的代碼再改進一下以解決這種情況新的Visitor的實現類叫NewReflectionVisitorImpl代碼如下所示



    public class NewReflectionVisitorImpl implements ReflectionVisitor {
        // 實現visit方法的類     
        private Object targetObject;    
        //構造方法傳入實現了visit方法的類
        public NewReflectionVisitorImpl(Object targetObject) {
            if (targetObject == null)
                throw new NullPointerException(
                        The target object should not be null!);
            thistargetObject = targetObject;
        }
        public void visitSoftwareComposite(Object softwareComposite) {
            //……與上個例子相同
            try {
                // 從目標的對象中查找visit方法
                Method m = targetObjectgetClass()getMethod(visit classes);
                // 調用該方法
                minvoke(targetObject objects);
            } catch (NoSuchMethodException e) {
                //……與上個例子相同
            } catch (Exception e) {
                //……與上個例子相同
            }
        }
    }
   該類的實現與上面的實現差不多多了一個構造函數在該構造函數的參數中傳入實現了visit方法的類並且維護了指向該類的一個引用另外最重要的地方是下面的兩行代碼



    // 從目標的對象中查找visit方法
    Method m = targetObjectgetClass()getMethod(visit classes);
    // 調用該方法
    minvoke(targetObject objects);

  本來的代碼中從本身的類及其子類中查找visit方法而現在是從維護的目標類中查找visit方法

  現在需要寫Tag類這個類擴展了TagSupport類如下所示(為說明的方便隨本文的例子提供了一個模擬的TagSupport類)



    public class MyTag extends TagSupport {
        SoftwareComponent softwareComponent = null;
        private double totalPrice = ;
        public int doEngTag() {
            //創建一個visitor對象並且將本身傳入visitor對象中
            ReflectionVisitor visitor = new NewReflectionVisitorImpl(this);
            //遍歷結構
            softwareComponentaccept(visitor);
            //打印出價格
            outprintln(totalPrice);
            return ;
        }
        //實現了針對Product的visit方法
        public void visit(Product product) {
            totalPrice += productgetPrice();
        }
        public void visit(Brand brand) {
            outprintln(brandgetId() + brandgetDescription());
        }
        //別的代碼請參見隨本文帶的源程序
        ……
    }
   如果想測試上面寫的那段代碼(在本文的例子comtestbusinessSoftwareManager中可以找到這段代碼))如下所示


    //getMockData()方法返回數據
    SoftwareComponent data = getMockData();
    MyTag myTag = new MyTag();
    myTagsetSoftwareComponent(data);
    //計算總價格並打印出來
    myTagdoEngTag();

  可以看到通過Java的反射機制很好地解決了多重繼承的問題使該訪問者模式能夠更好地應用於你的應用中另外可以看到那些visit方法所在的類已經不是實現了接口ReflectionVisitor可以說是訪問者模式在Java語言的支持下的一種特殊實現

  如果擔心引入類反射機制後帶來的效率問題你可以將Method對象通過某種方式緩沖起來這樣不會每次從傳入的對象中找visit方法可以部分地提高效率

  結論

  在給定的組合模式的數據結構中實現業務邏輯的方法非常多文中試著介紹了幾種實現業務邏輯的方法並給出了相應的實現方式下的優缺點讀者可以綜合考慮應用的需求來決定相應的實現方法


From:http://tw.wingwit.com/Article/program/Java/gj/201311/11152.html
    推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.