38 設(shè)計(jì)模式——訪問(wèn)者模式(Visitor模式)詳解

在現(xiàn)實(shí)生活中牲迫,有些集合對(duì)象存在多種不同的元素哩盲,且每種元素也存在多種不同的訪問(wèn)者和處理方式。例如月洛,公園中存在多個(gè)景點(diǎn)挂签,也存在多個(gè)游客炊邦,不同的游客對(duì)同一個(gè)景點(diǎn)的評(píng)價(jià)可能不同销斟;醫(yī)院醫(yī)生開的處方單中包含多種藥元素价卤,査看它的劃價(jià)員和藥房工作人員對(duì)它的處理方式也不同,劃價(jià)員根據(jù)處方單上面的藥品名和數(shù)量進(jìn)行劃價(jià)闯狱,藥房工作人員根據(jù)處方單的內(nèi)容進(jìn)行抓藥煞赢。

這樣的例子還有很多抛计,例如哄孤,電影或電視劇中的人物角色,不同的觀眾對(duì)他們的評(píng)價(jià)也不同吹截;還有顧客在商場(chǎng)購(gòu)物時(shí)放在“購(gòu)物車”中的商品瘦陈,顧客主要關(guān)心所選商品的性價(jià)比凝危,而收銀員關(guān)心的是商品的價(jià)格和數(shù)量。

這些被處理的數(shù)據(jù)元素相對(duì)穩(wěn)定而訪問(wèn)方式多種多樣的數(shù)據(jù)結(jié)構(gòu)晨逝,如果用“訪問(wèn)者模式”來(lái)處理比較方便蛾默。訪問(wèn)者模式能把處理方法從數(shù)據(jù)結(jié)構(gòu)中分離出來(lái),并可以根據(jù)需要增加新的處理方法捉貌,且不用修改原來(lái)的程序代碼與數(shù)據(jù)結(jié)構(gòu)支鸡,這提高了程序的擴(kuò)展性和靈活性。

模式的定義與特點(diǎn)

訪問(wèn)者(Visitor)模式的定義:將作用于某種數(shù)據(jù)結(jié)構(gòu)中的各元素的操作分離出來(lái)封裝成獨(dú)立的類趁窃,使其在不改變數(shù)據(jù)結(jié)構(gòu)的前提下可以添加作用于這些元素的新的操作牧挣,為數(shù)據(jù)結(jié)構(gòu)中的每個(gè)元素提供多種訪問(wèn)方式。它將對(duì)數(shù)據(jù)的操作與數(shù)據(jù)結(jié)構(gòu)進(jìn)行分離醒陆,是行為類模式中最復(fù)雜的一種模式瀑构。

訪問(wèn)者(Visitor)模式是一種對(duì)象行為型模式,其主要優(yōu)點(diǎn)如下刨摩。

  1. 擴(kuò)展性好寺晌。能夠在不修改對(duì)象結(jié)構(gòu)中的元素的情況下,為對(duì)象結(jié)構(gòu)中的元素添加新的功能澡刹。
  2. 復(fù)用性好呻征。可以通過(guò)訪問(wèn)者來(lái)定義整個(gè)對(duì)象結(jié)構(gòu)通用的功能像屋,從而提高系統(tǒng)的復(fù)用程度怕犁。
  3. 靈活性好。訪問(wèn)者模式將數(shù)據(jù)結(jié)構(gòu)與作用于結(jié)構(gòu)上的操作解耦己莺,使得操作集合可相對(duì)自由地演化而不影響系統(tǒng)的數(shù)據(jù)結(jié)構(gòu)奏甫。
  4. 符合單一職責(zé)原則。訪問(wèn)者模式把相關(guān)的行為封裝在一起凌受,構(gòu)成一個(gè)訪問(wèn)者阵子,使每一個(gè)訪問(wèn)者的功能都比較單一。

訪問(wèn)者(Visitor)模式的主要缺點(diǎn)如下胜蛉。

  1. 增加新的元素類很困難挠进。在訪問(wèn)者模式中,每增加一個(gè)新的元素類誊册,都要在每一個(gè)具體訪問(wèn)者類中增加相應(yīng)的具體操作领突,這違背了“開閉原則”。
  2. 破壞封裝案怯。訪問(wèn)者模式中具體元素對(duì)訪問(wèn)者公布細(xì)節(jié)君旦,這破壞了對(duì)象的封裝性。
  3. 違反了依賴倒置原則。訪問(wèn)者模式依賴了具體類金砍,而沒(méi)有依賴抽象類局蚀。

模式的結(jié)構(gòu)與實(shí)現(xiàn)

訪問(wèn)者(Visitor)模式實(shí)現(xiàn)的關(guān)鍵是如何將作用于元素的操作分離出來(lái)封裝成獨(dú)立的類,其基本結(jié)構(gòu)與實(shí)現(xiàn)方法如下恕稠。

1. 模式的結(jié)構(gòu)

訪問(wèn)者模式包含以下主要角色琅绅。

  1. 抽象訪問(wèn)者(Visitor)角色:定義一個(gè)訪問(wèn)具體元素的接口,為每個(gè)具體元素類對(duì)應(yīng)一個(gè)訪問(wèn)操作 visit() 鹅巍,該操作中的參數(shù)類型標(biāo)識(shí)了被訪問(wèn)的具體元素千扶。
  2. 具體訪問(wèn)者(ConcreteVisitor)角色:實(shí)現(xiàn)抽象訪問(wèn)者角色中聲明的各個(gè)訪問(wèn)操作,確定訪問(wèn)者訪問(wèn)一個(gè)元素時(shí)該做什么骆捧。
  3. 抽象元素(Element)角色:聲明一個(gè)包含接受操作 accept() 的接口县貌,被接受的訪問(wèn)者對(duì)象作為 accept() 方法的參數(shù)。
  4. 具體元素(ConcreteElement)角色:實(shí)現(xiàn)抽象元素角色提供的 accept() 操作凑懂,其方法體通常都是 visitor.visit(this) 煤痕,另外具體元素中可能還包含本身業(yè)務(wù)邏輯的相關(guān)操作。
  5. 對(duì)象結(jié)構(gòu)(Object Structure)角色:是一個(gè)包含元素角色的容器接谨,提供讓訪問(wèn)者對(duì)象遍歷容器中的所有元素的方法摆碉,通常由 List、Set脓豪、Map 等聚合類實(shí)現(xiàn)巷帝。

其結(jié)構(gòu)圖如圖 1 所示。

3-1Q11910135Y25.gif

圖1 訪問(wèn)者(Visitor)模式的結(jié)構(gòu)圖

2. 模式的實(shí)現(xiàn)

訪問(wèn)者模式的實(shí)現(xiàn)代碼如下:

package net.biancheng.c.visitor;
import java.util.*;
public class VisitorPattern {
    public static void main(String[] args) {
        ObjectStructure os = new ObjectStructure();
        os.add(new ConcreteElementA());
        os.add(new ConcreteElementB());
        Visitor visitor = new ConcreteVisitorA();
        os.accept(visitor);
        System.out.println("------------------------");
        visitor = new ConcreteVisitorB();
        os.accept(visitor);
    }
}
//抽象訪問(wèn)者
interface Visitor {
    void visit(ConcreteElementA element);
    void visit(ConcreteElementB element);
}
//具體訪問(wèn)者A類
class ConcreteVisitorA implements Visitor {
    public void visit(ConcreteElementA element) {
        System.out.println("具體訪問(wèn)者A訪問(wèn)-->" + element.operationA());
    }
    public void visit(ConcreteElementB element) {
        System.out.println("具體訪問(wèn)者A訪問(wèn)-->" + element.operationB());
    }
}
//具體訪問(wèn)者B類
class ConcreteVisitorB implements Visitor {
    public void visit(ConcreteElementA element) {
        System.out.println("具體訪問(wèn)者B訪問(wèn)-->" + element.operationA());
    }
    public void visit(ConcreteElementB element) {
        System.out.println("具體訪問(wèn)者B訪問(wèn)-->" + element.operationB());
    }
}
//抽象元素類
interface Element {
    void accept(Visitor visitor);
}
//具體元素A類
class ConcreteElementA implements Element {
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    public String operationA() {
        return "具體元素A的操作扫夜。";
    }
}
//具體元素B類
class ConcreteElementB implements Element {
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    public String operationB() {
        return "具體元素B的操作楞泼。";
    }
}
//對(duì)象結(jié)構(gòu)角色
class ObjectStructure {
    private List<Element> list = new ArrayList<Element>();
    public void accept(Visitor visitor) {
        Iterator<Element> i = list.iterator();
        while (i.hasNext()) {
            ((Element) i.next()).accept(visitor);
        }
    }
    public void add(Element element) {
        list.add(element);
    }
    public void remove(Element element) {
        list.remove(element);
    }
}

程序的運(yùn)行結(jié)果如下:

具體訪問(wèn)者A訪問(wèn)-->具體元素A的操作。
具體訪問(wèn)者A訪問(wèn)-->具體元素B的操作笤闯。
------------------------
具體訪問(wèn)者B訪問(wèn)-->具體元素A的操作堕阔。
具體訪問(wèn)者B訪問(wèn)-->具體元素B的操作。

模式的應(yīng)用實(shí)例

【例1】利用“訪問(wèn)者(Visitor)模式”模擬藝術(shù)公司與造幣公司的功能颗味。

分析:藝術(shù)公司利用“銅”可以設(shè)計(jì)出銅像超陆,利用“紙”可以畫出圖畫;造幣公司利用“銅”可以印出銅幣浦马,利用“紙”可以印出紙幣时呀。對(duì)“銅”和“紙”這兩種元素,兩個(gè)公司的處理方法不同晶默,所以該實(shí)例用訪問(wèn)者模式來(lái)實(shí)現(xiàn)比較適合谨娜。

首先,定義一個(gè)公司(Company)接口磺陡,它是抽象訪問(wèn)者趴梢,提供了兩個(gè)根據(jù)紙(Paper)或銅(Cuprum)這兩種元素創(chuàng)建作品的方法屎债;再定義藝術(shù)公司(ArtCompany)類和造幣公司(Mint)類,它們是具體訪問(wèn)者垢油,實(shí)現(xiàn)了父接口的方法。

然后圆丹,定義一個(gè)材料(Material)接口滩愁,它是抽象元素,提供了 accept(Company visitor)方法來(lái)接受訪問(wèn)者(Company)對(duì)象訪問(wèn)辫封;再定義紙(Paper)類和銅(Cuprum)類硝枉,它們是具體元素類,實(shí)現(xiàn)了父接口中的方法倦微。

最后妻味,定義一個(gè)材料集(SetMaterial)類,它是對(duì)象結(jié)構(gòu)角色欣福,擁有保存所有元素的容器 List责球,并提供讓訪問(wèn)者對(duì)象遍歷容器中的所有元素的 accept(Company visitor)方法;客戶類設(shè)計(jì)成窗體程序拓劝,它提供材料集(SetMaterial)對(duì)象供訪問(wèn)者(Company)對(duì)象訪問(wèn)雏逾,實(shí)現(xiàn)了 ItemListener 接口,處理用戶的事件請(qǐng)求郑临。圖 2 所示是其結(jié)構(gòu)圖栖博。

3-1Q119101J2P8.gif

圖2 藝術(shù)公司與造幣公司的結(jié)構(gòu)圖

程序代碼如下:

package net.biancheng.c.visitor;
import javax.swing.*;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class VisitorProducer {
    public static void main(String[] args) {
        new MaterialWin();
    }
}
//窗體類
class MaterialWin extends JFrame implements ItemListener {
    private static final long serialVersionUID = 1L;
    JPanel CenterJP;
    SetMaterial os;    //材料集對(duì)象
    Company visitor1, visitor2;    //訪問(wèn)者對(duì)象
    String[] select;
    MaterialWin() {
        super("利用訪問(wèn)者模式設(shè)計(jì)藝術(shù)公司和造幣公司");
        JRadioButton Art;
        JRadioButton mint;
        os = new SetMaterial();
        os.add(new Cuprum());
        os.add(new Paper());
        visitor1 = new ArtCompany();//藝術(shù)公司
        visitor2 = new Mint(); //造幣公司
        this.setBounds(10, 10, 750, 350);
        this.setResizable(false);
        CenterJP = new JPanel();
        this.add("Center", CenterJP);
        JPanel SouthJP = new JPanel();
        JLabel yl = new JLabel("原材料有:銅和紙,請(qǐng)選擇生產(chǎn)公司:");
        Art = new JRadioButton("藝術(shù)公司", true);
        mint = new JRadioButton("造幣公司");
        Art.addItemListener(this);
        mint.addItemListener(this);
        ButtonGroup group = new ButtonGroup();
        group.add(Art);
        group.add(mint);
        SouthJP.add(yl);
        SouthJP.add(Art);
        SouthJP.add(mint);
        this.add("South", SouthJP);
        select = (os.accept(visitor1)).split(" ");    //獲取產(chǎn)品名
        showPicture(select[0], select[1]);    //顯示產(chǎn)品
    }
    //顯示圖片
    void showPicture(String Cuprum, String paper) {
        CenterJP.removeAll();    //清除面板內(nèi)容
        CenterJP.repaint();    //刷新屏幕
        String FileName1 = "src/visitor/Picture/" + Cuprum + ".jpg";
        String FileName2 = "src/visitor/Picture/" + paper + ".jpg";
        JLabel lb = new JLabel(new ImageIcon(FileName1), JLabel.CENTER);
        JLabel rb = new JLabel(new ImageIcon(FileName2), JLabel.CENTER);
        CenterJP.add(lb);
        CenterJP.add(rb);
        this.setVisible(true);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
    @Override
    public void itemStateChanged(ItemEvent arg0) {
        JRadioButton jc = (JRadioButton) arg0.getSource();
        if (jc.isSelected()) {
            if (jc.getText() == "造幣公司") {
                select = (os.accept(visitor2)).split(" ");
            } else {
                select = (os.accept(visitor1)).split(" ");
            }
            showPicture(select[0], select[1]);    //顯示選擇的產(chǎn)品
        }
    }
}
//抽象訪問(wèn)者:公司
interface Company {
    String create(Paper element);
    String create(Cuprum element);
}
//具體訪問(wèn)者:藝術(shù)公司
class ArtCompany implements Company {
    public String create(Paper element) {
        return "講學(xué)圖";
    }
    public String create(Cuprum element) {
        return "朱熹銅像";
    }
}
//具體訪問(wèn)者:造幣公司
class Mint implements Company {
    public String create(Paper element) {
        return "紙幣";
    }
    public String create(Cuprum element) {
        return "銅幣";
    }
}
//抽象元素:材料
interface Material {
    String accept(Company visitor);
}
//具體元素:紙
class Paper implements Material {
    public String accept(Company visitor) {
        return (visitor.create(this));
    }
}
//具體元素:銅
class Cuprum implements Material {
    public String accept(Company visitor) {
        return (visitor.create(this));
    }
}
//對(duì)象結(jié)構(gòu)角色:材料集
class SetMaterial {
    private List<Material> list = new ArrayList<Material>();
    public String accept(Company visitor) {
        Iterator<Material> i = list.iterator();
        String tmp = "";
        while (i.hasNext()) {
            tmp += ((Material) i.next()).accept(visitor) + " ";
        }
        return tmp; //返回某公司的作品集
    }
    public void add(Material element) {
        list.add(element);
    }
    public void remove(Material element) {
        list.remove(element);
    }
}

程序運(yùn)行結(jié)果如圖 3 所示厢洞。

3-1Q119101U2436.jpg

(a)藝術(shù)公司設(shè)計(jì)的產(chǎn)品

3-1Q119101921H6.jpg

(b)造幣公司生產(chǎn)的貨幣

圖3 藝術(shù)公司與造幣公司的運(yùn)行結(jié)果

模式的應(yīng)用場(chǎng)景

當(dāng)系統(tǒng)中存在類型數(shù)量穩(wěn)定(固定)的一類數(shù)據(jù)結(jié)構(gòu)時(shí)仇让,可以使用訪問(wèn)者模式方便地實(shí)現(xiàn)對(duì)該類型所有數(shù)據(jù)結(jié)構(gòu)的不同操作,而又不會(huì)對(duì)數(shù)據(jù)產(chǎn)生任何副作用(臟數(shù)據(jù))躺翻。

簡(jiǎn)而言之丧叽,就是當(dāng)對(duì)集合中的不同類型數(shù)據(jù)(類型數(shù)量穩(wěn)定)進(jìn)行多種操作時(shí),使用訪問(wèn)者模式公你。

通常在以下情況可以考慮使用訪問(wèn)者(Visitor)模式蠢正。

  1. 對(duì)象結(jié)構(gòu)相對(duì)穩(wěn)定,但其操作算法經(jīng)常變化的程序省店。
  2. 對(duì)象結(jié)構(gòu)中的對(duì)象需要提供多種不同且不相關(guān)的操作嚣崭,而且要避免讓這些操作的變化影響對(duì)象的結(jié)構(gòu)。
  3. 對(duì)象結(jié)構(gòu)包含很多類型的對(duì)象懦傍,希望對(duì)這些對(duì)象實(shí)施一些依賴于其具體類型的操作雹舀。

模式的擴(kuò)展

訪問(wèn)者(Visitor)模式是使用頻率較高的一種設(shè)計(jì)模式,它常常同以下兩種設(shè)計(jì)模式聯(lián)用粗俱。

(1)與“迭代器模式”聯(lián)用说榆。因?yàn)樵L問(wèn)者模式中的“對(duì)象結(jié)構(gòu)”是一個(gè)包含元素角色的容器,當(dāng)訪問(wèn)者遍歷容器中的所有元素時(shí),常常要用迭代器签财。如【例1】中的對(duì)象結(jié)構(gòu)是用 List 實(shí)現(xiàn)的串慰,它通過(guò) List 對(duì)象的 Iterator() 方法獲取迭代器。如果對(duì)象結(jié)構(gòu)中的聚合類沒(méi)有提供迭代器唱蒸,也可以用迭代器模式自定義一個(gè)邦鲫。

(2)訪問(wèn)者(Visitor)模式同“組合模式”聯(lián)用。因?yàn)樵L問(wèn)者(Visitor)模式中的“元素對(duì)象”可能是葉子對(duì)象或者是容器對(duì)象神汹,如果元素對(duì)象包含容器對(duì)象庆捺,就必須用到組合模式,其結(jié)構(gòu)圖如圖 4 所示屁魏。

3-1Q11910210Jc.gif

圖4 包含組合模式的訪問(wèn)者模式的結(jié)構(gòu)圖

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末滔以,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子氓拼,更是在濱河造成了極大的恐慌,老刑警劉巖撬即,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剥槐,死亡現(xiàn)場(chǎng)離奇詭異宪摧,居然都是意外死亡粒竖,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門几于,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蕊苗,“玉大人,你說(shuō)我怎么就攤上這事沿彭⌒嗯椋” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵喉刘,是天一觀的道長(zhǎng)瞧柔。 經(jīng)常有香客問(wèn)我,道長(zhǎng)睦裳,這世上最難降的妖魔是什么造锅? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮廉邑,結(jié)果婚禮上哥蔚,老公的妹妹穿的比我還像新娘倒谷。我一直安慰自己,他們只是感情好糙箍,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布渤愁。 她就那樣靜靜地躺著,像睡著了一般深夯。 火紅的嫁衣襯著肌膚如雪抖格。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音站辉,去河邊找鬼饰剥。 笑死,一個(gè)胖子當(dāng)著我的面吹牛顾孽,可吹牛的內(nèi)容都是我干的若厚。 我是一名探鬼主播测秸,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼钞瀑,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼关串!你這毒婦竟也來(lái)了晋修?” 一聲冷哼從身側(cè)響起墓卦,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎呢堰,沒(méi)想到半個(gè)月后枉疼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡航闺,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年稠肘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了滑黔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片略荡。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡粥谬,死狀恐怖漏策,靈堂內(nèi)的尸體忽然破棺而出芭届,到底是詐尸還是另有隱情褂乍,我是刑警寧澤逃片,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布洁仗,位于F島的核電站叫胖,受9級(jí)特大地震影響瓮增,放射性物質(zhì)發(fā)生泄漏绷跑。R本人自食惡果不足惜砸捏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望掂骏。 院中可真熱鬧弟灼,春花似錦蠕趁、人聲如沸辛馆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至棚点,卻和暖如春瘫析,著一層夾襖步出監(jiān)牢的瞬間贬循,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工奇适, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人间影。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓巩割,卻偏偏與公主長(zhǎng)得像宣谈,于是被迫代替她去往敵國(guó)和親闻丑。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容