访问者(Visitor)模式

在现实生活中,有些集合对象中存在多种不同的元素,且每种元素也存在多种不同的访问者和处理方式。例如,公园中存在多个景点,也存在多个游客,不同的游客对同一个景点的评价可能不同;医院医生开的处方单中包含多种药元素,査看它的划价员和药房工作人员对它的处理方式也不同,划价员根据处方单上面的药品名和数量进行划价,药房工作人员根据处方单的内容进行抓药。

这样的例子还有很多,例如,电影或电视剧中的人物角色,不同的观众对他们的评价也不同;还有顾客在商场购物时放在“购物车”中的商品,顾客主要关心所选商品的性价比,而收银员关心的是商品的价格和数量。

这些被处理的数据元素相对稳定而访问方式多种多样的数据结构,如果用“访问者模式”来处理比较方便。访问者模式能把处理方法从数据结构中分离出来,并可以根据需要增加新的处理方法,且不用修改原来的程序代码与数据结构,这提高了程序的扩展性和灵活性。

模式的定义与特点

访问者(Visitor)模式的定义:将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构进行分离,是行为类模式中最复杂的一种模式。

访问者(Visitor)模式是一种对象行为型模式,其主要优点如下。

  1. 扩展性好。能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
  2. 复用性好。可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。
  3. 灵活性好。访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。
  4. 符合单一职责原则。访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。

访问者(Visitor)模式的主要缺点如下。

  1. 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
  2. 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。
  3. 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。

模式的结构与实现

访问者(Visitor)模式实现的关键是如何将作用于元素的操作分离出来封装成独立的类,其基本结构与实现方法如下。

1. 模式的结构

访问者模式包含以下主要角色。

  1. 抽象访问者(Visitor)角色:定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作 visit() ,该操作中的参数类型标识了被访问的具体元素。
  2. 具体访问者(ConcreteVisitor)角色:实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。
  3. 抽象元素(Element)角色:声明一个包含接受操作 accept() 的接口,被接受的访问者对象作为 accept() 方法的参数。
  4. 具体元素(ConcreteElement)角色:实现抽象元素角色提供的 accept() 操作,其方法体通常都是 visitor.visit(this) ,另外具体元素中可能还包含本身业务逻辑的相关操作。
  5. 对象结构(Object Structure)角色:是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map 等聚合类实现。

其结构图如图 1 所示。


图1 访问者(Visitor)模式的结构图(点此查看原图

2. 模式的实现

访问者模式的实现代码如下:

package  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);
       }
}

//抽象访问者
interface Visitor
{
            void visit(ConcreteElementA  element);
            void visit(ConcreteElementB  element);
}


//具体访问者A类
class  ConcreteVisitorA implements Visitor
 {
public void visit(ConcreteElementA  element)
{
System.out.println("具体访问者A访问-->"+element.operationA());
}
public void visit(ConcreteElementB  element)
 {
System.out.println("具体访问者A访问-->"+element.operationB());
 }
 }
 //具体访问者B类
 class  ConcreteVisitorB implements Visitor
{
 public void visit(ConcreteElementA  element)
{
System.out.println("具体访问者B访问-->"+element.operationA());
}
 public void visit(ConcreteElementB  element)
{
System.out.println("具体访问者B访问-->"+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的操作。";
}
}
 //对象结构角色
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);
 }
 }

程序的运行结果如下:

<pre class="info-box">具体访问者A访问-->具体元素A的操作。
具体访问者A访问-->具体元素B的操作。


具体访问者B访问-->具体元素A的操作。
具体访问者B访问-->具体元素B的操作。</pre>

模式的应用实例

【例1】利用“访问者(Visitor)模式”模拟艺术公司与造币公司的功能。

分析:艺术公司利用“铜”可以设计出铜像,利用“纸”可以画出图画;造币公司利用“铜”可以印出铜币,利用“纸”可以印出纸币(点此下载运行该程序后所要显示的图片)。对“铜”和“纸”这两种元素,两个公司的处理方法不同,所以该实例用访问者模式来实现比较适合。

首先,定义一个公司(Company)接口,它是抽象访问者,提供了两个根据纸(Paper)或铜(Cuprum)这两种元素创建作品的方法;再定义艺术公司(ArtCompany)类和造币公司(Mint)类,它们是具体访问者,实现了父接口的方法;然后,定义一个材料(Material)接口,它是抽象元素,提供了 accept(Company visitor)方法来接受访问者(Company)对象访问;再定义纸(Paper)类和铜(Cuprum)类,它们是具体元素类,实现了父接口中的方法;最后,定义一个材料集(SetMaterial)类,它是对象结构角色,拥有保存所有元素的容器 List,并提供让访问者对象遍历容器中的所有元素的 accept(Company visitor)方法;客户类设计成窗体程序,它提供材料集(SetMaterial)对象供访问者(Company)对象访问,实现了 ItemListener 接口,处理用户的事件请求。图 2 所示是其结构图。

艺术公司与造币公司的结构图

图2 艺术公司与造币公司的结构图

程序代码如下:


1.  package  visitor;
2.  import  java.awt.event.*;
3.  import  java.util.*;
4.  import  javax.swing.*;
5.  public class  VisitorProducer
6.  {
7.  public static void main(String[] args)
8.  {
9.  new MaterialWin();       
10.  }
11.  }
12.  //窗体类
13.  class  MaterialWin extends JFrame implements ItemListener
14.  {
15.  private static final long serialVersionUID=1L;   
16.  JPanel  CenterJP;
17.  SetMaterial  os;    //材料集对象
18.  Company  visitor1,visitor2;    //访问者对象
19.  String[] select;
20.  MaterialWin()
21.  {
22.  super("利用访问者模式设计艺术公司和造币公司");
23.  JRadioButton  Art;
24.  JRadioButton  mint;           
25.  os=new SetMaterial();     
26.  os.add(new Cuprum());
27.  os.add(new Paper());
28.  visitor1=new ArtCompany();//艺术公司
29.  visitor2=new Mint(); //造币公司 
30.  this.setBounds(10,10,750,350);            
31.  this.setResizable(false);       
32.  CenterJP=new JPanel();       
33.  this.add("Center",CenterJP);      
34.  JPanel  SouthJP=new JPanel();
35.  JLabel  yl=new JLabel("原材料有:铜和纸,请选择生产公司:");
36.  Art=new JRadioButton("艺术公司",true);
37.  mint=new JRadioButton("造币公司");
38.  Art.addItemListener(this);
39.  mint.addItemListener(this);       
40.  ButtonGroup  group=new ButtonGroup();
41.  group.add(Art);
42.  group.add(mint);
43.  SouthJP.add(yl);
44.  SouthJP.add(Art);
45.  SouthJP.add(mint);
46.  this.add("South",SouthJP);       
47.  select=(os.accept(visitor1)).split(" ");    //获取产品名
48.  showPicture(select[0],select[1]);    //显示产品
49.  }
50.  //显示图片
51.  void showPicture(String  Cuprum,String  paper)
52.  {
53.  CenterJP.removeAll();    //清除面板内容
54.  CenterJP.repaint();    //刷新屏幕
55.  String  FileName1="src/visitor/Picture/"+Cuprum+".jpg";
56.  String  FileName2="src/visitor/Picture/"+paper+".jpg";
57.  JLabel  lb=new JLabel(new ImageIcon(FileName1),JLabel.CENTER);
58.  JLabel  rb=new JLabel(new ImageIcon(FileName2),JLabel.CENTER);
59.  CenterJP.add(lb);
60.  CenterJP.add(rb);
61.  this.setVisible(true);
62.  this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);            
63.  }
64.  @Override
65.  public void itemStateChanged(ItemEvent  arg0)
66.  {
67.  JRadioButton  jc=(JRadioButton) arg0.getSource();
68.  if (jc.isSelected())
69.  {
70.  if (jc.getText()=="造币公司")
71.  {
72.  select=(os.accept(visitor2)).split(" ");
73.  }
74.  else
75.  {            
76.  select=(os.accept(visitor1)).split(" ");
77.  }
78.  showPicture(select[0],select[1]);    //显示选择的产品
79.  }    
80.  }
81.  }
82.  //抽象访问者:公司
83.  interface Company
84.  {
85.  String  create(Paper  element);
86.  String  create(Cuprum  element);
87.  }
88.  //具体访问者:艺术公司
89.  class  ArtCompany implements Company
90.  {
91.  public String  create(Paper  element)
92.  {
93.  return "讲学图";
94.  }
95.  public String  create(Cuprum  element)
96.  {
97.  return "朱熹铜像";
98.  }
99.  }
100.  //具体访问者:造币公司
101.  class  Mint implements Company
102.  {
103.  public String  create(Paper  element)
104.  {
105.  return "纸币";
106.  }
107.  public String  create(Cuprum  element)
108.  {
109.  return "铜币";
110.  }
111.  }
112.  //抽象元素:材料
113.  interface Material
114.  {
115.  String  accept(Company  visitor);
116.  }
117.  //具体元素:纸
118.  class  Paper implements Material
119.  {
120.  public String  accept(Company  visitor)
121.  {
122.  return(visitor.create(this));
123.  }
124.  }
125.  //具体元素:铜
126.  class  Cuprum implements Material
127.  {
128.  public String  accept(Company  visitor)
129.  {
130.  return(visitor.create(this));
131.  }
132.  }
133.  //对象结构角色:材料集
134.  class  SetMaterial
135.  {   
136.  private List<Material>  list=new ArrayList<Material>();   
137.  public String  accept(Company  visitor)
138.  {
139.  Iterator<Material>  i=list.iterator();
140.  String  tmp="";
141.  while(i.hasNext())
142.  {
143.  tmp+=((Material) i.next()).accept(visitor)+" ";
144.  }
145.  return tmp; //返回某公司的作品集 
146.  }
147.  public void add(Material  element)
148.  {
149.  list.add(element);
150.  }
151.  public void remove(Material  element)
152.  {
153.  list.remove(element);
154.  }
155.  }

程序运行结果如图 3 所示。

艺术公司设计的产品

(a)艺术公司设计的产品

造币公司生产的货币

(b)造币公司生产的货币

图3 艺术公司与造币公司的运行结果

模式的应用场景

通常在以下情况可以考虑使用访问者(Visitor)模式。

  1. 对象结构相对稳定,但其操作算法经常变化的程序。
  2. 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。
  3. 对象结构包含很多类型的对象,希望对这些对象实施一些依赖于其具体类型的操作。

模式的扩展

访问者(Visitor)模式是使用频率较高的一种设计模式,它常常同以下两种设计模式联用。

(1)与“迭代器模式”联用。因为访问者模式中的“对象结构”是一个包含元素角色的容器,当访问者遍历容器中的所有元素时,常常要用迭代器。如【例1】中的对象结构是用 List 实现的,它通过 List 对象的 Itemtor() 方法获取迭代器。如果对象结构中的聚合类没有提供迭代器,也可以用迭代器模式自定义一个。

(2)访问者(Visitor)模式同“组合模式”联用。因为访问者(Visitor)模式中的“元素对象”可能是叶子对象或者是容器对象,如果元素对象包含容器对象,就必须用到组合模式,其结构图如图 4 所示。

包含组合模式的访问者模式的结构图

图4 包含组合模式的访问者模式的结构图

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容