1. 程式人生 > >(二十二)訪問者模式詳解(偽動態雙分派)

(二十二)訪問者模式詳解(偽動態雙分派)

 作者:zuoxiaolong8810(左瀟龍),轉載請註明出處,特別說明:本博文來自博主原部落格,為保證新部落格中博文的完整性,特複製到此留存,如需轉載請註明新部落格地址即可。

              本次LZ和各位分享一下訪問者模式,從場景、設計初衷以及實現方面來說,訪問者模式算是LZ即將寫到的24種設計模式當中,最複雜也是最難理解的一個設計模式。

              針對這樣一個設計模式,LZ到底該如何和各位分享呢?

              

              廢話不多說,我們先來看看訪問者模式的定義。

              定義(源於GoF《Design Pattern》):表示一個作用於某物件結構中的各元素的操作。它使你可以在不改變各元素類的前提下定義作用於這些元素的新操作。

              初次接觸,定義會顯得晦澀並且難於理解,沒關係,LZ來陪著各位一起一點一點分析定義中所提到的關鍵點。

              先來看第一句話,說是一個作用於某物件結構中的各元素的操作,這裡提到了三個事物,一個是物件結構,一個是各元素,一個是操作。那麼我們可以這麼理解,有這麼一個操作,它是作用於一些元素之上的,而這些元素屬於某一個物件結構。

              好了,最關鍵的第二句來了,它說使用了訪問者模式之後,可以讓我們在不改變各元素類的前提下定義作用於這些元素的新操作。這裡面的關鍵點在於前半句,即不改變各元素類的前提下,在這個前提下定義新操作是訪問者模式精髓中的精髓。

              下面我們來看看訪問者模式的類圖,去尋找一下訪問者模式的角色都有哪些。

                  我們從類圖中可以找到五類角色,兩個介面,兩種實現類以及一個物件結構。

                  Visitor介面:它定義了對每一個元素(Element)訪問的行為,它的引數就是可以訪問的元素,它的方法個數理論上來講與元素個數(Element的實現類個數)是一樣的,從這點不難看出,訪問者模式要求元素類的個數不能改變(不能改變的意思是說,如果元素類的個數經常改變,則說明不適合使用訪問者模式)。

                  ConcreteVisitor:具體的訪問者,它需要給出對每一個元素類訪問時所產生的具體行為。

                  Element介面:元素介面,它定義了一個接受訪問者(accept)的方法,其意義是指,每一個元素都要可以被訪問者訪問。

                  ConcreteElement:具體的元素類,它提供接受訪問方法的具體實現,而這個具體的實現,通常情況下是使用訪問者提供的訪問該元素類的方法。

                  ObjectStructure:這個便是定義當中所提到的物件結構,物件結構是一個抽象表述,具體點可以理解為一個具有容器性質或者複合物件特性的類,它會含有一組元素(Element),並且可以迭代這些元素,供訪問者訪問。

                  

                  在上面五個角色當中,最重要的就是最後一個,所謂的訪問者模式,就是為了讓訪問者可以方便的訪問物件結構而存在的。關於訪問者模式的例子,很多文章和文獻使用男人和女人的例子,所以LZ這裡就不重複了。

                  我們來看一個財務方面的簡單例子,我們都知道財務都是有賬本的,這個賬本就可以作為一個物件結構,而它其中的元素有兩種,收入和支出,這滿足我們訪問者模式的要求,即元素的個數是穩定的,因為賬本中的元素只能是收入和支出,就像人只有男人和女人一樣(儘管現在貌似經常出現第三種,具體第三種是什麼請大家自行補腦)。

                  賬本的訪問者都會有哪些呢?

                  LZ不才,對財務只有一個直觀的理解,不過這已經足夠我們來做例子了,依LZ的瞭解,檢視賬本的人可能有這樣幾種,比如老闆,會計事務所的注會,財務主管,等等。而這些人在看賬本的時候顯然目的和行為是不同的。

                  我們接下來就試著寫一下這個例子,首先我們給出單子的介面,它只有一個方法accept。

複製程式碼

package com.visitor;
//單個單子的介面(相當於Element)
public interface Bill {

    void accept(AccountBookViewer viewer);
    
}

複製程式碼

                其中的方法引數AccountBookViewer是一個賬本訪問者介面,這個介面稍後給出,我們先給出兩個具體的元素,也就是收入單子和消費單子,或者說收入和支出類。

複製程式碼

package com.visitor;

//消費的單子
public class ConsumeBill implements Bill{

    private double amount;
    
    private String item;
    
    public ConsumeBill(double amount, String item) {
        super();
        this.amount = amount;
        this.item = item;
    }

    public void accept(AccountBookViewer viewer) {
        viewer.view(this);
    }

    public double getAmount() {
        return amount;
    }

    public String getItem() {
        return item;
    }

}

複製程式碼

複製程式碼

package com.visitor;

//收入單子
public class IncomeBill implements Bill{

    private double amount;
    
    private String item;
    
    public IncomeBill(double amount, String item) {
        super();
        this.amount = amount;
        this.item = item;
    }
    
    public void accept(AccountBookViewer viewer) {
        viewer.view(this);
    }

    public double getAmount() {
        return amount;
    }

    public String getItem() {
        return item;
    }

}

複製程式碼

                這兩個類沒什麼特別的,為了使得例子更加形象貼近實際,LZ加入了兩個屬性,一個是金額,一個是單子的專案,而最關鍵的還是裡面的accept方法,它直接讓訪問者訪問自己,這相當於一次靜態分派,當然我們也可以不使用過載而直接給方法不同的名稱,這些有關分派的知識LZ稍後介紹。

                下面我們給出剛才出現過的賬本訪問者介面,它有兩個方法,如下。

複製程式碼

package com.visitor;

//賬單檢視者介面(相當於Visitor)
public interface AccountBookViewer {

    //檢視消費的單子
    void view(ConsumeBill bill);
    
    //檢視收入的單子
    void view(IncomeBill bill);
    
}

複製程式碼

                這兩個方法是過載方法,就是在上面的元素類當中用到的,當然你也可以按照訪問者模式類圖當中的方式去做,將兩個方法分別命名為viewConsumeBill和viewIncomeBill,而一般是建議按照類圖上來做的,不過無論怎麼寫,這並不影響訪問者模式的使用。

                下面我們給出兩個訪問者的例子,當然訪問者可能會有很多,但是作為例子,我們並不需要寫太多,這些訪問者都需要實現上面的介面,並且提供兩個view方法,也就是他們針對消費的單子和收入的單子都分別要做些什麼。

複製程式碼

package com.visitor;

//老闆類,檢視賬本的類之一
public class Boss implements AccountBookViewer{
    
    private double totalIncome;
    
    private double totalConsume;
    
    //老闆只關注一共花了多少錢以及一共收入多少錢,其餘並不關心
    public void view(ConsumeBill bill) {
        totalConsume += bill.getAmount();
    }

    public void view(IncomeBill bill) {
        totalIncome += bill.getAmount();
    }

    public double getTotalIncome() {
        System.out.println("老闆檢視一共收入多少,數目是:" + totalIncome);
        return totalIncome;
    }

    public double getTotalConsume() {
        System.out.println("老闆檢視一共花費多少,數目是:" + totalConsume);
        return totalConsume;
    }
    
}

複製程式碼

複製程式碼

package com.visitor;

//註冊會計師類,檢視賬本的類之一
public class CPA implements AccountBookViewer{

    //注會在看賬本時,如果是支出,則如果支出是工資,則需要看應該交的稅交了沒
    public void view(ConsumeBill bill) {
        if (bill.getItem().equals("工資")) {
            System.out.println("注會檢視賬本時,如果單子的消費目的是發工資,則注會會檢視有沒有交個人所得稅。");
        }
    }
    //如果是收入,則所有的收入都要交稅
    public void view(IncomeBill bill) {
        System.out.println("注會檢視賬本時,只要是收入,注會都要檢視公司交稅了沒。");
    }

}

複製程式碼

                可以看到,這兩個類有巨大的差異,老闆只關心收入和支出的總額,而注會只關注該交稅的是否交稅,當然現實當中,二者可能關注的不只是這些,甚至完全不是這些,不過作為例子,這並不是我們關注的重點。

                下面該出場的是我們最重要的一個類,賬本類,它是當前訪問者模式例子中的物件結構,如下。

複製程式碼

package com.visitor;

import java.util.ArrayList;
import java.util.List;

//賬本類(相當於ObjectStruture)
public class AccountBook {
    //單子列表
    private List<Bill> billList = new ArrayList<Bill>();
    //新增單子
    public void addBill(Bill bill){
        billList.add(bill);
    }
    //供賬本的檢視者檢視賬本
    public void show(AccountBookViewer viewer){
        for (Bill bill : billList) {
            bill.accept(viewer);
        }
    }
}

複製程式碼

                可以看到,我們的賬本類當中有一個列表,這個列表是元素(Bill)的集合,這便是物件結構的通常表示,它一般會是一堆元素的集合,不過這個集合不一定是列表,也可能是樹,連結串列等等任何資料結構,甚至是若干個資料結構。

                有一些文章當中的例子,物件結構還有remove方法,不過這裡由於賬本比較特殊,是不能刪除的,所以為了在簡單的基礎上儘量與實際情況貼近,LZ就沒有加入remove方法。至於show方法,就是賬本類的精髓了,它會列舉每一個元素,讓訪問者訪問。

                下面我們給出一個簡單的客戶端,測試一下這個訪問者模式。

複製程式碼

package com.visitor;

public class Client {

    public static void main(String[] args) {
        AccountBook accountBook = new AccountBook();
        //新增兩條收入
        accountBook.addBill(new IncomeBill(10000, "賣商品"));
        accountBook.addBill(new IncomeBill(12000, "賣廣告位"));
        //新增兩條支出
        accountBook.addBill(new ConsumeBill(1000, "工資"));
        accountBook.addBill(new ConsumeBill(2000, "材料費"));
        
        AccountBookViewer boss = new Boss();
        AccountBookViewer cpa = new CPA();
        
        //兩個訪問者分別訪問賬本
        accountBook.show(cpa);
        accountBook.show(boss);
        
        ((Boss) boss).getTotalConsume();
        ((Boss) boss).getTotalIncome();
    }
}

複製程式碼

 

                 可以看到,兩個訪問者老闆和注會對賬本的檢視,行為是完全不同的,但是這正是訪問者模式的意義所在,它其實是將訪問者這部分邏輯獨立出去,讓其自生自滅。我們可以直觀的去理解,上面的程式碼中,賬本以及賬本中的元素是非常穩定的,這些幾乎不可能改變,而最容易改變的就是訪問者這部分。

                 訪問者模式最大的優點就是增加訪問者非常容易,我們從程式碼上來看,如果要增加一個訪問者,你只需要做一件事即可,那就是寫一個類,實現AccountBookViewer介面,然後就可以直接呼叫AccountBook的show方法去訪問賬本了。

                 很簡單吧!

                 如果沒使用訪問者模式,各位可以自己寫一下,你一定會在show方法中寫一堆if else,就算不是在show方法,你也一定逃不掉這些if else,它們一定會出現。而且每增加一個訪問者,你都需要改你的if else,程式碼會顯得非常臃腫,而且非常難以擴充套件和維護。這裡由於本章內容較多,限於篇幅,LZ就不給各位寫了。

                 在這裡,LZ給出上面的例子的類圖供各位參考,類圖與標準的類圖一模一樣。

                 

                 下面LZ結合上面的例子,給各位分析一下百度百科中對訪問者模式的一些描述。

                 訪問者模式的幾個特點:

                 1、訪問者模式把資料結構和作用於結構上的操作解耦合,使得操作集合可相對自由地演化。

                 2、訪問者模式適用於資料結構相對穩定演算法又易變化的系統。因為訪問者模式使得演算法操作增加變得容易。若系統資料結構物件易於變化,經常有新的資料物件增加進來,則不適合使用訪問者模式。

                 3、訪問者模式的優點是增加操作很容易,因為增加操作意味著增加新的訪問者。訪問者模式將有關行為集中到一個訪問者物件中,其改變不影響系統資料結構。其缺點就是增加新的資料結構很困難。

                 第一點,資料結構和作用於結構上的操作解耦合,使操作集合可以相對自由的演化,這在上面的例子當中指的是,我們把賬本以及賬本的元素與檢視賬本的人解耦,使得這些訪問者的行為可以相對獨立的變化,這點其實並不難理解。這一點其實說的是訪問者模式的優點。

                 至於剩下的兩點,開始提到訪問者模式適用於資料結構相對穩定,而演算法行為又易變化的系統,這點不難理解,試想一下,如果賬本結構不穩定,經常有元素加進來,那麼假設有了第三種非支出也非收入的單子,那我們需要做以下兩件事。

                 1)新增一個類ABill,實現Bill介面。

                 2)在AccountBookViewer介面中新增一個方法view(ABill bill),並且在所有AccountBookViewer介面的實現類中都增加view(ABill bill)方法的具體實現。

                 這其中第一件事並不難,而且也符合開閉原則,但是第二件事就值得商榷了。它修改了抽象,導致所有細節都跟著變化,這完全破壞了開閉原則。所以第二點說使用訪問者模式的前提是資料結構相對穩定也就不奇怪了。

                 然而對於演算法操作,在訪問者模式的使用下,我們可以自由的新增,這個在上面已經提及到,也就是說我們如果要增加檢視賬本的類,是非常簡單的,我們只需要寫一個類去實現AccountBookViewer介面,這是開閉原則的完美詮釋。

                 訪問者模式中,元素的新增會破壞開閉原則,訪問者的新增又符合開閉原則,所以有文獻稱該模式是傾斜的開閉原則,即一邊是符合開閉原則的,一邊又是破壞了開閉原則的,有點傾斜的感覺。

                 

                 從上面的描述,我們可以大致總結出訪問者模式的優缺點以及適用性。

                 優點:

                 1、使得資料結構和作用於結構上的操作解耦,使得操作集合可以獨立變化。

                 2、新增新的操作或者說訪問者會非常容易。

                 3、將對各個元素的一組操作集中在一個訪問者類當中。

                 4、使得類層次結構不改變的情況下,可以針對各個層次做出不同的操作,而不影響類層次結構的完整性。

                 5、可以跨越類層次結構,訪問不同層次的元素類,做出相應的操作。

                 缺點:

                 1、增加新的元素會非常困難。

                 2、實現起來比較複雜,會增加系統的複雜性。

                 3、破壞封裝,如果將訪問行為放在各個元素中,則可以不暴露元素的內部結構和狀態,但使用訪問者模式的時候,為了讓訪問者能獲取到所關心的資訊,元素類不得不暴露出一些內部的狀態和結構,就像收入和支出類必須提供訪問金額和單子的專案的方法一樣。
                 適用性:

                 1、資料結構穩定,作用於資料結構的操作經常變化的時候。

                 2、當一個數據結構中,一些元素類需要負責與其不相關的操作的時候,為了將這些操作分離出去,以減少這些元素類的職責時,可以使用訪問者模式。

                 3、有時在對資料結構上的元素進行操作的時候,需要區分具體的型別,這時使用訪問者模式可以針對不同的型別,在訪問者類中定義不同的操作,從而去除掉型別判斷。

                 

                 上面優點當中的前三點比較好理解,缺點和適用性當中提到的三點也比較好理解,如果優點當中的後兩點不理解也沒關係,LZ會在後面給出簡單的例子。

                 在給出後兩點優點的例子之前,不得不先解釋一下前面提到的分派,分派按照分派的方式可以分為靜態分派和動態分派,按照宗量或者說判斷依據的多少,可以分為單分派和多分派,下面LZ簡單的介紹下這幾種分派方式。

                 

                 靜態分派以及多分派:

                 靜態分派就是按照變數的靜態型別進行分派,從而確定方法的執行版本,靜態分派在編譯時期就可以確定方法的版本。而靜態分派最典型的應用就是方法過載,考慮下面一段程式。

複製程式碼

public class Main {

    public void test(String string){
        System.out.println("string");
    }
    
    public void test(Integer integer){
        System.out.println("integer");
    }
    
    public static void main(String[] args) {
        String string = "1";
        Integer integer = 1;
        Main main = new Main();
        main.test(integer);
        main.test(string);
    }
}

複製程式碼

                相信執行結果不需要LZ給出了,會依次列印integer和string,對於test方法,會根據靜態型別決定方法版本,而所判斷的依據就是,在main型別確定之後,依據test方法的引數型別和引數數量,我們就可以唯一的確定一個過載方法的版本。比如上面的例子,我們確定完main的型別之後,就可以根據test方法是一個引數,並且這個引數是Integer型別還是String型別,就可以確定到底呼叫哪個過載方法了。

                可以看到,在靜態分派判斷的時候,我們根據多個判斷依據(即引數型別和個數)判斷出了方法的版本,那麼這個就是多分派的概念,因為我們有一個以上的考量標準,也可以稱為宗量。所以JAVA是靜態多分派的語言。

                動態分派以及單分派:
                對於動態分派,與靜態相反,它不是在編譯期確定的方法版本,而是在執行時才能確定。而動態分派最典型的應用就是多型的特性,考慮下面一段程式。

複製程式碼

package com.visitor1;

interface Person{
    void test();
}
class Man implements Person{
    public void test(){
        System.out.println("男人");
    }
}
class Woman implements Person{
    public void test(){
        System.out.println("女人");
    }
}
public class Main {
    
    public static void main(String[] args) {
        Person man = new Man();
        Person woman = new Woman();
        man.test();
        woman.test();
    }
}

複製程式碼

                這段程式輸出結果為依次列印男人和女人,這應該是所有人都預料到的,然而現在的test方法版本,就無法根據man和woman的靜態型別去判斷了,他們的靜態型別都是Person介面,根本無從判斷。

                顯然,產生的輸出結果,就是因為test方法的版本是在執行時判斷的,這就是動態分派。

                動態分派判斷的方法是在執行時獲取到man和woman的實際引用型別,再確定方法的版本,而由於此時判斷的依據只是實際引用型別,只有一個判斷依據,所以這就是單分派的概念,這時我們的考量標準只有一個宗量,即變數的實際引用型別。相應的,這說明JAVA是動態單分派的語言。

                

                訪問者模式中的偽動態雙分派:
                上面LZ已經簡單的介紹了JAVA語言中的靜態多分派和動態單分派,更詳細的解釋各位可以在其它文獻和文章中尋找,這裡我們談談訪問者模式的分派方式。

                標題上已經註明了,訪問者模式中使用的是偽動態雙分派,之所以加了一個“偽”字,是因為一個模式當然不可能更改語言的特性,所以JAVA是動態單分派的語言這點毋庸置疑,而訪問者模式只是利用了一些手段達到了看似雙分派的效果。

                對於動態雙分派這個詞語,初次接觸的讀友或許比較迷惑,LZ先給大家一個初步的解釋,動態雙分派就是在執行時依據兩個實際型別去判斷一個方法的執行行為,而訪問者模式實現的手段是進行了兩次動態單分派來達到這個效果。

                我們來看上面例子當中賬本類中的accept方法的呼叫。

for (Bill bill : billList) {
            bill.accept(viewer);
        }

                上面說了訪問者模式是使用兩次動態單分派達到了依據兩個實際型別在執行時判斷一個方法版本的效果,那麼對於我們現在的例子來說,就是依據biil和viewer兩個實際型別決定了view方法的版本,從而決定了accept方法的動作。請注意LZ的用詞,是決定accept方法的動作以及決定了view方法的版本。

                為什麼要強調是accept方法的動作而不是方法的版本,是因為accept方法的版本只需要一次動態分派就可以確定,但是它所產生的動作卻需要兩次動態分派才能確定。

                我們來看下這個accept方法的呼叫過程,LZ還是分步驟給各位解釋。

                1、當呼叫accept方法時,根據bill的實際型別決定是呼叫ConsumeBill還是IncomeBill的accept方法。

                2、這時accept方法的版本已經確定,假如是ConsumeBill,它的accept方法是呼叫下面這行程式碼。

    public void accept(AccountBookViewer viewer) {
        viewer.view(this);
    }

                此時的this是ConsumeBill型別,所以對應於AccountBookViewer介面的view(ConsumeBill bill)方法,此時需要再根據viewer的實際型別確定view方法的版本,如此一來,就完成了動態雙分派的過程。

                以上的過程就是通過兩次動態雙分派,第一次對accept方法進行動態分派,第二次對view(類圖中的visit方法)方法進行動態分派,從而達到了根據兩個實際型別確定一個方法的行為的效果。

                而原本我們的做法,通常是傳入一個介面,直接使用該介面的方法,此為動態單分派,就像策略模式一樣。在這裡,show方法傳入的viewer介面並不是直接呼叫自己的view方法,而是通過bill的實際型別先動態分派一次,然後在分派後確定的方法版本里再進行自己的動態分派。

 

                在此之外,還需要再解釋一點,在上面第2步,確定view(ConsumeBill bill)方法是靜態分派決定的,所以這個並不在此次動態雙分派的範疇內,而且靜態分派是在編譯期就完成的,也就是說,在上述第1步之前就已經完成了對view(ConsumeBill bill)方法版本的選取。況且把靜態分派算在內的話,由於靜態分派是多分派,這裡就不能叫雙分派了,應該叫動態多分派,這顯然是不成立的。所以view(ConsumeBill bill)方法的靜態分派與訪問者模式的動態雙分派並沒有任何關係。

                而且退一步講,我們完全可以將AccountBookViewer介面中的兩個view方法取不同的名字,這樣也就完全避免了方法版本確定中靜態分派參與的嫌疑,而且這完全不影響訪問者模式的效果,可以清楚的看到,標準類圖中也是這麼建議的。這裡LZ寫成一樣的名字,只是為了方便和更加清晰,而且在只有兩個方法的時候這麼做也並無不可,但是如果方法多的時候,強烈建議不要取一樣的名字,由於靜態分派的過載版本往往不是唯一的,所以過載版本過多會造成一定的干擾。

                LZ的例子只是為了更加清晰的展示訪問者模式,在實際應用中,還是強烈建議各位使用不同的方法名稱去命名各個元素的訪問方法。

                在這裡LZ解釋靜態分派不參與雙分派的原因,是因為看到不少文章都多多少少在暗示訪問者模式的雙分派與view(this)的靜態分派相關,這個觀點恕LZ不能苟同,動態雙分派說到底還是動態分派,是在執行時發生的,它與靜態分派有著本質上的區別,不可以說一次動態分派加一次靜態分派就是動態雙分派,而且訪問者模式的雙分派本身也是另有所指。

                你可能會說,this的型別是動態確定的,這麼算下來不也是根據AccountBookViewer的實際型別和Bill的實際型別做了一次動態雙分派嗎?

                答案是當然不能這麼算,this的型別可不是動態確定的,你寫在哪個類當中,它的靜態型別就是哪個類,這是在編譯期就確定的,不確定的是它的實際型別,請各位區分開這一點。

                如果各位只是為了暫時理解訪問者模式的雙分派,這樣理解倒也不是不可,以後隨著理解的加深,會漸漸更加清晰。不過如果你這麼理解訪問者模式的雙分派,一定要搞清楚一點,那就是this的型別是在編譯期就可以確定的,而不是在執行時動態確定的,它並不是真正的動態繫結,而且說到底它始終是一個引數型別,參與分派的是它的靜態型別,依舊是靜態分派的範疇。總之怎麼理解是自己的事,LZ只是試圖點破一下這裡面的分派關係。

 

                 上面的分析算是一個鋪墊,至於後面對於靜態分派的分析,各位可以當做一個討論,也可以在文章下方發表評論參與進來,LZ不勝歡迎。

                 接下來我們還有最後一件事沒完成了,那就是前面提到的優點的後兩點的體現,因為在前面的例子當中沒有用到,所以可能會理解起來會有點困難,這裡LZ把上面的例子優化一下,讓各位體驗一下優點中的後兩點是多麼強大。

                 我們先來考慮一個問題,假設我們上面的例子當中再新增一個財務主管,而財務主管不管你是支出還是收入,都要詳細的檢視你的單子的專案以及金額,簡單點說就是財務主管類的兩個view方法的程式碼是一樣的。

                 你可能會說,這好辦啊,我們可以複製一下嘛。是的,這當然是一個辦法,不過相信對程式碼有要求的程式猿們一定不喜歡他們的程式碼裡出現一堆的重複程式碼。而且假設有很多人的訪問方式和財務主管一樣,對收入和支出的操作一樣,那得複製多少程式碼。

                 解決方案就是我們可以將元素提煉出層次結構,針對層次結構提供操作的方法,這樣就實現了優點當中最後兩點提到的針對層次定義操作以及跨越層次定義操作。

                 這次LZ跟以前的步驟反過來一下,先給出LZ設計的類圖,然後我們再來一步步實現針對層次結構做出的訪問者模式。

                    可以看出,我們針對剛才的實現體系,分別抽象出一個層次,現在我們便可以針對層次定義操作了,左邊的CFO(財務主管類)便是針對高層次定義的操作,它會統一作用於AbstractBill。

                    為了支援層次操作,我們需要在處於最低層次的元素類當中新增一些層次結構的條件判斷,不過這個是可以接受的,因為這段程式碼不會被更改,這是符合開閉原則的。

                    下面LZ直接將程式碼貼上來,各位可以從中體會一下層次的感覺。

複製程式碼

package com.visitor;
//單個單子的介面(相當於Element)
public interface Bill {

    void accept(Viewer viewer);
    
}

複製程式碼

複製程式碼

package com.visitor;

//抽象單子類,一個高層次的單子抽象
public abstract class AbstractBill implements Bill{
    
    protected double amount;
    
    protected String item;
    
    public AbstractBill(double amount, String item) {
        super();
        this.amount = amount;
        this.item = item;
    }
    
    public double getAmount() {
        return amount;
    }

    public String getItem() {
        return item;
    }
    
}

複製程式碼

複製程式碼

package com.visitor;

//收入單子
public class IncomeBill extends AbstractBill{
    
    public IncomeBill(double amount, String item) {
        super(amount, item);
    }

    public void accept(Viewer viewer) {
        if (viewer instanceof AbstractViewer) {
            ((AbstractViewer)viewer).viewIncomeBill(this);
            return;
        }
        viewer.viewAbstractBill(this);
    }

}

複製程式碼

複製程式碼

package com.visitor;

//消費的單子
public class ConsumeBill extends AbstractBill{

    public ConsumeBill(double amount, String item) {
        super(amount, item);
    }

    public void accept(Viewer viewer) {
        if (viewer instanceof AbstractViewer) {
            ((AbstractViewer)viewer).viewConsumeBill(this);
            return;
        }
        viewer.viewAbstractBill(this);
    }

}

複製程式碼

                這是元素類的層次結構,可以看到,我們的accept當中出現了if判斷,各位看到可能會疑惑了,不是說不讓用if else嗎,這裡怎麼又出現了?

                請各位仔細體會這裡的條件判斷的意義,它不是在判斷一個介面或者類的具體型別,而是在判斷一個層次,基於這一點,這個判斷是穩定的,就像上面說的一樣,通俗點說,這段程式碼是不會被更改的。

                下面LZ再把分層之後的訪問者層次貼上來。

複製程式碼

package com.visitor;

//超級訪問者介面(它支援定義高層操作)
public interface Viewer{

    void viewAbstractBill(AbstractBill bill);
    
}

複製程式碼

複製程式碼

package com.visitor;

//比Viewer介面低一個層次的訪問者介面
public abstract class AbstractViewer implements Viewer{

    //檢視消費的單子
    abstract void viewConsumeBill(ConsumeBill bill);
    
    //檢視收入的單子
    abstract void viewIncomeBill(IncomeBill bill);
    
    public final void viewAbstractBill(AbstractBill bill){}
}

複製程式碼

複製程式碼

package com.visitor;

//老闆類,檢視賬本的類之一,作用於最低層次結構
public class Boss extends AbstractViewer{
    
    private double totalIncome;
    
    private double totalConsume;
    
    //老闆只關注一共花了多少錢以及一共收入多少錢,其餘並不關心
    public void viewConsumeBill(ConsumeBill bill) {
        totalConsume += bill.getAmount();
    }

    public void viewIncomeBill(IncomeBill bill) {
        totalIncome += bill.getAmount();
    }

    public double getTotalIncome() {
        System.out.println("老闆檢視一共收入多少,數目是:" + totalIncome);
        return totalIncome;
    }

    public double getTotalConsume() {
        System.out.println("老闆檢視一共花費多少,數目是:" + totalConsume);
        return totalConsume;
    }
    
}

複製程式碼

複製程式碼

package com.visitor;

//註冊會計師類,檢視賬本的類之一,作用於最低層次結構
public class CPA extends AbstractViewer{

    //注會在看賬本時,如果是支出,則如果支出是工資,則需要看應該交的稅交了沒
    public void viewConsumeBill(ConsumeBill bill) {
        if (bill.getItem().equals("工資")) {
            System.out.println("注會檢視賬本時,如果單子的消費目的是發工資,則注會會檢視有沒有交個人所得稅。");
        }
    }
    //如果是收入,則所有的收入都要交稅
    public void viewIncomeBill(IncomeBill bill) {
        System.out.println("注會檢視賬本時,只要是收入,注會都要檢視公司交稅了沒。");
    }

}

複製程式碼

複製程式碼

package com.visitor;

//財務主管類,檢視賬本的類之一,作用於高層的層次結構
public class CFO implements Viewer {

    //財務主管對每一個單子都要核對專案和金額
    public void viewAbstractBill(AbstractBill bill) {
        System.out.println("財務主管檢視賬本時,每一個都核對專案和金額,金額是" + bill.getAmount() + ",專案是" + bill.getItem());
    }

}

複製程式碼

                可以看到,LZ這次為了避免迷惑,將view方法的名字全都給改了,在這種情況下,如果你仍然使用相同的名字,那麼就算你對靜態分派特別熟悉,在層次較多的情況下也會被繞暈的,而且最關鍵的是,我們改下名字完全不影響訪問者模式的效果。

                這裡要說一下的是,財務主管(CFO)是針對AbstractBill這一層定義的操作,而原來的老闆(Boss)和註冊會計師(CPA)都是針對ConsumeBill和IncomeBill這一層定義的操作,這時已經產生了跨越層次結構的行為,老闆和註冊會計師都跨過了抽象單子這一層,直接針對具體的單子定義操作。

                我們的賬本類是完全不需要變化的,不過為了方便觀看,這裡再次貼上來。

複製程式碼

package com.visitor;

import java.util.ArrayList;
import java.util.List;

//賬本類(相當於ObjectStruture)
public class AccountBook {
    //單子列表
    private List<Bill> billList = new ArrayList<Bill>();
    //新增單子
    public void addBill(Bill bill){
        billList.add(bill);
    }
    //供賬本的檢視者檢視賬本
    public void show(Viewer viewer){
        for (Bill bill : billList) {
            bill.accept(viewer);
        }
    }
}

複製程式碼

                下面我們就寫一個客戶端,來看一下針對層次定義的操作是什麼效果。

複製程式碼

package com.visitor;

public class Client {

    public static void main(String[] args) {
        AccountBook accountBook = new AccountBook();
        //新增兩條收入
        accountBook.addBill(new IncomeBill(10000, "賣商品"));
        accountBook.addBill(new IncomeBill(12000, "賣廣告位"));
        //新增兩條支出
        accountBook.addBill(new ConsumeBill(1000, "工資"));
        accountBook.addBill(new ConsumeBill(2000, "材料費"));
        
        Viewer boss = new Boss();
        Viewer cpa = new CPA();
        Viewer cfo = new CFO();
        
        //兩個訪問者分別訪問賬本
        accountBook.show(cpa);
        accountBook.show(boss);
        accountBook.show(cfo);
        
        ((Boss) boss).getTotalConsume();
        ((Boss) boss).getTotalIncome();
    }
}

複製程式碼

                 可以看到,我們的注會和老闆會針對收入和支出做出不同的操作,而我們財務主管則對所有的單子都是一樣的處理方法。

                 這下好了,如果再出現和財務主管一樣對所有單子都是一樣操作的人,我們就不需要複製程式碼了,只需要讓他實現Viewer介面就可以了,而如果要像老闆和注會一樣區分單子的具體型別,則繼承AbstractViewer就可以。

                 目前我們的元素類是兩層結構,所以我們現在可以針對這兩層定義操作,如果元素類的結構是三層、四層或者N層,我們依然可以使用同樣的手法達到現在的效果,比如將收入和支出類再寫成抽象類,然後支出又分為出差,工資等等,收入又分為現金收入,債務收入等等,隨便怎麼分都可以,總之就是出現了三層元素類。那麼我們就需要將viewer再抽象一層,從而支援三層定義。

                 從上面這個例子,我們就可以體會到訪問者模式的另外一些優點了,即上面提到的第4和第5點。

                 這裡針對層次結構做出的更改是基於靜態分派的分派規則,即沒有符合與方法引數列表相同的方法,則會將方法引數自動向上轉型去匹配方法版本。

                 

                 好了,訪問者模式已經介紹了不少了,到現在也基本上該結束了。本章的文章有點長,這也是實屬無奈,因為訪問者模式牽扯的內容比較多,而且這還是LZ儘量縮短文章長度的結果。

                 各位如果有興趣,可以去私下試一下支援三層操作定義怎麼做,而且對於動態雙分派的問題也可以在下方留言討論。

                 本次文章就到此吧,十分感謝各位的收看。

                

 

 

 

 

版權宣告

 


作者:zuoxiaolong(左瀟龍)

出處:部落格園左瀟龍的技術部落格--http://www.cnblogs.com/zuoxiaolong

您的支援是對博主最大的鼓勵,感謝您的認真閱讀。

本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。