在現(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)如下刨摩。
- 擴(kuò)展性好寺晌。能夠在不修改對(duì)象結(jié)構(gòu)中的元素的情況下,為對(duì)象結(jié)構(gòu)中的元素添加新的功能澡刹。
- 復(fù)用性好呻征。可以通過(guò)訪問(wèn)者來(lái)定義整個(gè)對(duì)象結(jié)構(gòu)通用的功能像屋,從而提高系統(tǒng)的復(fù)用程度怕犁。
- 靈活性好。訪問(wèn)者模式將數(shù)據(jù)結(jié)構(gòu)與作用于結(jié)構(gòu)上的操作解耦己莺,使得操作集合可相對(duì)自由地演化而不影響系統(tǒng)的數(shù)據(jù)結(jié)構(gòu)奏甫。
- 符合單一職責(zé)原則。訪問(wèn)者模式把相關(guān)的行為封裝在一起凌受,構(gòu)成一個(gè)訪問(wèn)者阵子,使每一個(gè)訪問(wèn)者的功能都比較單一。
訪問(wèn)者(Visitor)模式的主要缺點(diǎn)如下胜蛉。
- 增加新的元素類很困難挠进。在訪問(wèn)者模式中,每增加一個(gè)新的元素類誊册,都要在每一個(gè)具體訪問(wèn)者類中增加相應(yīng)的具體操作领突,這違背了“開閉原則”。
- 破壞封裝案怯。訪問(wèn)者模式中具體元素對(duì)訪問(wèn)者公布細(xì)節(jié)君旦,這破壞了對(duì)象的封裝性。
- 違反了依賴倒置原則。訪問(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)者模式包含以下主要角色琅绅。
- 抽象訪問(wèn)者(Visitor)角色:定義一個(gè)訪問(wèn)具體元素的接口,為每個(gè)具體元素類對(duì)應(yīng)一個(gè)訪問(wèn)操作 visit() 鹅巍,該操作中的參數(shù)類型標(biāo)識(shí)了被訪問(wèn)的具體元素千扶。
- 具體訪問(wèn)者(ConcreteVisitor)角色:實(shí)現(xiàn)抽象訪問(wèn)者角色中聲明的各個(gè)訪問(wèn)操作,確定訪問(wèn)者訪問(wèn)一個(gè)元素時(shí)該做什么骆捧。
- 抽象元素(Element)角色:聲明一個(gè)包含接受操作 accept() 的接口县貌,被接受的訪問(wèn)者對(duì)象作為 accept() 方法的參數(shù)。
- 具體元素(ConcreteElement)角色:實(shí)現(xiàn)抽象元素角色提供的 accept() 操作凑懂,其方法體通常都是 visitor.visit(this) 煤痕,另外具體元素中可能還包含本身業(yè)務(wù)邏輯的相關(guān)操作。
- 對(duì)象結(jié)構(gòu)(Object Structure)角色:是一個(gè)包含元素角色的容器接谨,提供讓訪問(wèn)者對(duì)象遍歷容器中的所有元素的方法摆碉,通常由 List、Set脓豪、Map 等聚合類實(shí)現(xiàn)巷帝。
其結(jié)構(gòu)圖如圖 1 所示。
圖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)圖栖博。
圖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 所示厢洞。
(a)藝術(shù)公司設(shè)計(jì)的產(chǎn)品
(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)模式蠢正。
- 對(duì)象結(jié)構(gòu)相對(duì)穩(wěn)定,但其操作算法經(jīng)常變化的程序省店。
- 對(duì)象結(jié)構(gòu)中的對(duì)象需要提供多種不同且不相關(guān)的操作嚣崭,而且要避免讓這些操作的變化影響對(duì)象的結(jié)構(gòu)。
- 對(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 所示屁魏。
圖4 包含組合模式的訪問(wèn)者模式的結(jié)構(gòu)圖