訪問者模式

引子

訪問者模式在23種設(shè)計模式中應(yīng)該算是最復(fù)雜也是最難以理解的一種模式了,因此在解釋的時候我不打算從定義說起,以實際的例子帶入可能會比較好吧。

例子

我們的例子是交通局需要對外公布所有交通工具的票價,假設(shè)現(xiàn)在交通局只負(fù)責(zé)管理公交車和火車的事兒,實際上也許他們不止管理這點東西

已有系統(tǒng)代碼

代碼枯燥但是也是必要的,交通工具抽象成接口如下

public interface Vehicle {
    int getBasePrice();
}

兩個子類

public class Bus implements Vehicle {
    @Override
    public int getBasePrice() {
        return 50;
    }
}

public class Train implements Vehicle {
    @Override
    public int getBasePrice() {
        return 100;
    }
}

系統(tǒng)的正事不要忘記,交通局要公布票價的

public class Manager {
    
    List<Vehicle> vehicles = new ArrayList<Vehicle>();
    
    public void add(Vehicle vehicle) {
        vehicles.add(vehicle);
    }
    
    public void remove(Vehicle vehicle) {
        vehicles.remove(vehicle);
    }
    
    public void printAllPrice() {
        for (Vehicle vehicle : vehicles) {
            System.out.println(vehicle.getBasePrice());
        }
    }
    
    public static void main(String[] args) {
        // 構(gòu)造交通局
        Manager manager = new Manager();
        
        // 初始化,即添加交通局要管理的所有交通工具
        manager.add(new Bus());
        manager.add(new Train());
        
        // 公布票價
        manager.printAllPrice();
    }
}

新需求

新年到了,票價變了,交通局要公布新年票價了,每個交通工具漲價幅度不一樣,對原系統(tǒng)要做修改了。
首先是交通工具接口,新增一個方法

public interface Vehicle {

    int getBasePrice();
    
    int getNewYearPrice();
}

然后子類要修改,去實現(xiàn)這個方法

public class Bus implements Vehicle {
    
    @Override
    public int getBasePrice() {
        return 50;
    }

    @Override
    public int getNewYearPrice() {
        return getBasePrice() * 2;
    }

}

public class Train implements Vehicle {

    @Override
    public int getBasePrice() {
        return 100;
    }

    @Override
    public int getNewYearPrice() {
        return getBasePrice() * 3;
    }
}

好像漲價漲的有點兇,暫時不去吐槽這個問題,來看看交通局怎么辦

public class Manager {
    
    List<Vehicle> vehicles = new ArrayList<Vehicle>();
    
    public void add(Vehicle vehicle) {
        vehicles.add(vehicle);
    }
    
    public void remove(Vehicle vehicle) {
        vehicles.remove(vehicle);
    }
    
    public void printNewYearPrice() {
        for (Vehicle vehicle : vehicles) {
            System.out.println(vehicle.getNewYearPrice());
        }
    }
    
    public static void main(String[] args) {
        // 構(gòu)造交通局
        Manager manager = new Manager();
        
        // 初始化,即添加交通局要管理的所有交通工具
        manager.add(new Bus());
        manager.add(new Train());
        
        // 公布新年票價
        manager.printNewYearPrice();
    }

}

大功告成

新需求+1+2+N

春節(jié)到了,交通局要公布春節(jié)票價了,這回可能要漲的更多了,畢竟春運(yùn)嘛。。。兒童節(jié)到了,要公布兒童專屬票價……

public interface Vehicle {

    int getBasePrice();
    
    int getNewYearPrice();

    int getXxxPrice();

    int getYyyPrice();

    int getZzzPrice

    // ......
}

我們發(fā)現(xiàn),這種修改方法會導(dǎo)致接口內(nèi)的方法需要被修改,然后每一個子類也都需要做相應(yīng)改動,如果子類不止Bus和Train的話,這種體力勞動我覺得應(yīng)該是能讓人喝一壺的,這時候我們就可以考慮使用訪問者來重構(gòu)這個系統(tǒng)了。

訪問者

現(xiàn)在我依然不去解釋什么是訪問者模式,但是我要說的是為了使用訪問者模式,我們需要在交通工具接口里新增一個方法accept,該方法接受訪問者作為參數(shù),子類實現(xiàn)時則通過這個訪問者參數(shù)訪問自身

這個過程的子類通用的實現(xiàn)就是Visitor.visit(this)

訪問者重構(gòu)系統(tǒng)

還是通過代碼來看吧,下面是接口,可以看到增加了一個以Visitor作為參數(shù)的accept方法

public interface Vehicle {

    void accept(IVisitor visitor);

    int getBasePrice();
}

兩個子類實現(xiàn)

public class Bus implements Vehicle {
    
    @Override
    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }

    @Override
    public int getBasePrice() {
        return 50;
    }
}

public class Train implements Vehicle {

    @Override
    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }

    @Override
    public int getBasePrice() {
        return 100;
    }
}

那這個

visitor.visit(this)

到底是什么東西呢,來看看訪問者的的實現(xiàn)就明白了
訪問者有一個接口,它定義了在訪問不同的交通工具時可以有的不同方法,我們有兩個交通工具,因此這個接口里有兩個方法

public interface IVisitor {
    
    void visit(Bus bus);
    
    void visit(Train train);
}

管理局要打印票價,那么作為交通工具的訪問者,可以滿足這個需求,我們實現(xiàn)一個訪問者類

public class BasePricePrinter implements IVisitor {

    @Override
    public void visit(Bus bus) {
        System.out.println(bus.getBasePrice());
    }

    @Override
    public void visit(Train train) {
        System.out.println(train.getBasePrice());
    }
}

這個訪問者類在訪問不同的交通工具時就是簡單的打印票價而已,那交通局要做的事情就簡單啦

public class Manager {
    
    List<Vehicle> vehicles = new ArrayList<Vehicle>();
    
    public void add(Vehicle vehicle) {
        vehicles.add(vehicle);
    }
    
    public void remove(Vehicle vehicle) {
        vehicles.remove(vehicle);
    }
    
    public void printPrice(IVisitor visitor) {
        for (Vehicle vehicle : vehicles) {
            vehicle.accept(visitor);
        }
    }
    
    public static void main(String[] args) {
        // 構(gòu)造交通局
        Manager manager = new Manager();
        
        // 初始化,即添加交通局要管理的所有交通工具
        manager.add(new Bus());
        manager.add(new Train());
        
        // 構(gòu)造一個基礎(chǔ)票價的打印器訪問者
        IVisitor basePricePrinter = new BasePricePrinter();
        
        // 公布票價
        manager.printPrice(basePricePrinter);
    }
}

交通局通過一個小弟基礎(chǔ)票價訪問者搞定了自己要做的所有事情,看看這個過程都在printPrice方法中,遍歷了所有交通工具,對每一個交通工具都使用基礎(chǔ)票價訪問者去訪問他們,基礎(chǔ)票價訪問者負(fù)責(zé)處理交通局派給它的任務(wù)——公布基礎(chǔ)票價

新需求

新年到了,交通局需要公布新年票價,我們要做的僅僅是添加一個新年票價訪問者的類就可以了

public class NewYearPricePrinter implements IVisitor {

    @Override
    public void visit(Bus bus) {
        System.out.println(bus.getBasePrice() * 2);
    }

    @Override
    public void visit(Train train) {
        System.out.println(train.getBasePrice() * 3);
    }

}

交通局的大爺不需要任何代碼改動,派出新年票價訪問者小弟解決所有事情

public static void main(String[] args) {
        // 構(gòu)造交通局
        Manager manager = new Manager();
        
        // 初始化,即添加交通局要管理的所有交通工具
        manager.add(new Bus());
        manager.add(new Train());
        
        // 構(gòu)造一個新年票價的打印器訪問者
        IVisitor newYearPricePrinter = new NewYearPricePrinter();
        
        // 公布新年票價
        manager.printPrice(basePricePrinter);
}

可以看到,這種情況下我們的改動量很少,當(dāng)需要公布春節(jié)票價,公布啥時候的票價都非常簡單,只需要添加一個訪問者就可以實現(xiàn)了。

定義

例子好長,終于講完了,回頭可以看看定義了,《設(shè)計模式》一書對于訪問者模式給出的定義是

表示一個作用于某對象結(jié)構(gòu)中的各元素的操作。它使你可以在不改變各元素的類的前提下定義作用于這些元素的新操作。

從定義可以看出結(jié)構(gòu)對象是使用訪問者模式必須條件,而且這個結(jié)構(gòu)對象必須存在遍歷自身各個對象的方法。
在我們的訪問者模式案例中,對象結(jié)構(gòu)就是交通局管理的一堆交通工具,我們在使用訪問者模式的時候的確是在新增對這些交通工具的操作,而且這些操作絲毫沒有改變這些交通工具的類。

組成結(jié)構(gòu)

訪問者模式中一共有五個角色(除去Client)

  • 抽象元素角色(Vehicle):定義一些Accept操作,它以一個訪問者為參數(shù),指定元素可以被哪些訪問者訪問。在我們的例子中就是Vehicle
  • 具體元素角色(Bus,Train):包含的方法分為兩部分,一部分是Accept操作,這部分是實現(xiàn)抽線元素角色所必須的,指定自身可以被哪些訪問者訪問,其代碼實現(xiàn)往往是簡單的一句話(visitor.visit(this)),另一部分是包含自身的業(yè)務(wù)邏輯方法,具體元素不同,這部分也可以各不相同。在我們的例子中就是Bus和Train了
  • 對象結(jié)構(gòu)角色(Manager):這是使用訪問者模式必備的角色。它要具備以下特征:能枚舉它的元素;可以提供一個高層的接口以允許該訪問者訪問它的元素;可以是一個復(fù)合(組合模式)或是一個集合,如一個列表或一個無序集合。我們可以把交通局當(dāng)做一個對象結(jié)構(gòu)角色。
  • 抽象訪問者角色(IVisitor):為對象結(jié)構(gòu)角色的每一類對象(Bus,Train)都聲明一個訪問操作結(jié)構(gòu),該操作接口的名字和參數(shù)標(biāo)識了發(fā)送訪問請求給具體訪問者的具體元素角色。
  • 具體訪問者角色(BasePricePrinter,NewYearPricePrinter):實現(xiàn)具體訪問時要做的操作

類圖

這個我就略過了,我想網(wǎng)絡(luò)上應(yīng)該有很多就不去畫了。

優(yōu)缺點

首先可以看到,使用訪問者模式之后,對于原來的元素增加新的操作僅僅只需要實現(xiàn)一個新的訪問者角色就行,而不比修改整個元素結(jié)構(gòu)體(不需要去修改交通工具這個結(jié)構(gòu)體所有實現(xiàn)類了),這樣符合『開閉原則』的要求。而且由于每個訪問者都對應(yīng)于一個相關(guān)操作,所以如果這個操作變了,那么僅僅只需要修改這個具體訪問者就行,比如例子里漲價幅度實在是太大了,群眾鬧變扭,就可以把價格調(diào)低一點。

訪問者模式最適用的是元素結(jié)構(gòu)變動不大的情況,即在可以預(yù)見的未來交通局還只能管理到汽車和火車的情況,如果哪天交通局權(quán)力變大了,要管UFO了,可以想象我們需要添加一個UFO類繼承于Vehicle,這倒還好,關(guān)鍵是所有的訪問者麻煩了,需要在抽象訪問者中添加對UFO的處理,所有實現(xiàn)訪問者也需要相應(yīng)增加處理,感覺似乎回到了一開始我們面臨的問題。除此之外,訪問者模式需要讓元素暴露自己的內(nèi)部屬性,也是他的缺點之一。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容