1. 访问者模式简介

访问者模式,是行为型设计模式之一。访问者模式是一种将数据操作与数据结构分离的设计模式,它可以算是 23 中设计模式中最复杂的一个,但它的使用频率并不是很高,大多数情况下,你并不需要使用访问者模式,但是当你一旦需要使用它时,那你就是需要使用它了。

访问者模式的基本想法是,软件系统中拥有一个由许多对象构成的、比较稳定的对象结构,这些对象的类都拥有一个 accept 方法用来接受访问者对象的访问。访问者是一个接口,它拥有一个 visit 方法,这个方法对访问到的对象结构中不同类型的元素做出不同的处理。在对象结构的一次访问过程中,我们遍历整个对象结构,对每一个元素都实施 accept 方法,在每一个元素的 accept 方法中会调用访问者的 visit 方法,从而使访问者得以处理对象结构的每一个元素,我们可以针对对象结构设计不同的访问者类来完成不同的操作,达到区别对待的效果。

2. 定义及使用场景

定义:封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作。

可以对定义这么理解:有这么一个操作,它是作用于一些元素之上的,而这些元素属于某一个对象结构。同时这个操作是在不改变各元素类的前提下,在这个前提下定义新操作是访问者模式精髓中的精髓。

使用场景:

  1. 对象结构比较稳定,但经常需要在此对象结构上定义新的操作。

  2. 需要对一个对象结构中的对象进行很多不同的且不相关的操作,而需要避免这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。

3. UML图

  1. Visitor:接口或者抽象类,它定义了对每一个元素(Element)访问的行为,它的参数就是可以访问的元素,它的方法数理论上来讲与元素个数是一样的,因此,访问者模式要求元素的类族要稳定,如果经常添加、移除元素类,必然会导致频繁地修改Visitor接口,如果这样则不适合使用访问者模式。

  2. ConcreteVisitor1、ConcreteVisitor2:具体的访问类,它需要给出对每一个元素类访问时所产生的具体行为。

  3. Element:元素接口或者抽象类,它定义了一个接受访问者的方法(Accept),其意义是指每一个元素都要可以被访问者访问。

  4. ConcreteElementA、ConcreteElementB:具体的元素类,它提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。

  5. ObjectStructure:定义当中所说的对象结构,对象结构是一个抽象表述,它内部管理了元素集合,并且可以迭代这些元素供访问者访问。

4. 访问者模式的简单例子

我们都知道财务都是有账本的,这个账本就可以作为一个对象结构,而它其中的元素有两种,收入和支出,这满足我们访问者模式的要求,即元素的个数是稳定的,因为账本中的元素只能是收入和支出。

而查看账本的人可能有这样几种,比如老板,会计事务所的注会,财务主管,等等。而这些人在看账本的时候显然目的和行为是不同的。

首先我们给出单子的接口,它只有一个方法accept。

//单个单子的接口(相当于Element)
public interface Bill {void accept(AccountBookViewer viewer);
}

其中的方法参数AccountBookViewer是一个账本访问者接口,后面会定义。

接下来也就是实现类,收入单子和消费单子,或者说收入和支出类。

//消费的单子
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;}}
//收入单子
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;}}

面最关键的还是里面的accept方法,它直接让访问者访问自己,这相当于一次静态分派(文章最后进行解释),当然我们也可以不使用重载而直接给方法不同的名称。

接下来是账本访问者接口:

//账单查看者接口(相当于Visitor)
public interface AccountBookViewer {//查看消费的单子void view(ConsumeBill bill);//查看收入的单子void view(IncomeBill bill);}

这两个方法是重载方法,就是在上面的元素类当中用到的,当然你也可以按照访问者模式类图当中的方式去做,将两个方法分别命名为viewConsumeBill和viewIncomeBill,而一般建议按照类图上来做的。

访问者的实现:

//老板类,查看账本的类之一
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;}}
//注册会计师类,查看账本的类之一
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("注会查看收入交税了没。");}}

老板只关心收入和支出的总额,而注会只关注该交税的是否交税。

接下来是账本类,它是当前访问者模式例子中的对象结构:

/账本类(相当于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)的集合,这便是对象结构的通常表示,它一般会是一堆元素的集合,不过这个集合不一定是列表,也可能是树,链表等等任何数据结构,甚至是若干个数据结构。其中show方法,就是账本类的精髓,它会枚举每一个元素,让访问者访问。

测试客户端

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方法去访问账本了。

如果没使用访问者模式,一定会增加许多if else,而且每增加一个访问者,你都需要改你的if else,代码会显得非常臃肿,而且非常难以扩展和维护。

5. 静态分派以及动态分派

变量被声明时的类型叫做变量的静态类型(Static Type),有些人又把静态类型叫做明显类型(Apparent Type);而变量所引用的对象的真实类型又叫做变量的实际类型(Actual Type)。比如:

List list = null;
list = new ArrayList();

声明了一个变量list,它的静态类型(也叫明显类型)是List,而它的实际类型是ArrayList。根据对象的类型而对方法进行的选择,就是分派(Dispatch),分派(Dispatch)又分为两种,即静态分派和动态分派。静态分派(Static Dispatch)发生在编译时期,分派根据静态类型信息发生。

5.1. 静态分派

静态分派就是按照变量的静态类型进行分派,从而确定方法的执行版本,静态分派在编译时期就可以确定方法的版本。而静态分派最典型的应用就是方法重载:

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

在静态分派判断的时候,我们根据多个判断依据(即参数类型和个数)判断出了方法的版本,那么这个就是多分派的概念,因为我们有一个以上的考量标准,也可以称为宗量。所以JAVA是静态多分派的语言。

5.2. 动态分派

对于动态分派,与静态相反,它不是在编译期确定的方法版本,而是在运行时才能确定。而动态分派最典型的应用就是多态的特性。

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是动态单分派的语言。

6. 访问者模式中的伪动态双分派

访问者模式中使用的是伪动态双分派,所谓的动态双分派就是在运行时依据两个实际类型去判断一个方法的运行行为,而访问者模式实现的手段是进行了两次动态单分派来达到这个效果。

回到上面例子当中账本类中的accept方法

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

这里就是依据biil和viewer两个实际类型决定了view方法的版本,从而决定了accept方法的动作。

分析accept方法的调用过程

  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的实际类型先动态分派一次,然后在分派后确定的方法版本里再进行自己的动态分派。

注意:这里确定view(ConsumeBill bill)方法是静态分派决定的,所以这个并不在此次动态双分派的范畴内,而且静态分派是在编译期就完成的,所以view(ConsumeBill bill)方法的静态分派与访问者模式的动态双分派并没有任何关系。动态双分派说到底还是动态分派,是在运行时发生的,它与静态分派有着本质上的区别,不可以说一次动态分派加一次静态分派就是动态双分派,而且访问者模式的双分派本身也是另有所指。

这里的this的类型不是动态确定的,你写在哪个类当中,它的静态类型就是哪个类,这是在编译期就确定的,不确定的是它的实际类型,请各位区分开这一点。

7. 总结

优点:

1、使得数据结构和作用于结构上的操作解耦,使得操作集合可以独立变化。

2、添加新的操作或者说访问者会非常容易。

3、将对各个元素的一组操作集中在一个访问者类当中。

4、使得类层次结构不改变的情况下,可以针对各个层次做出不同的操作,而不影响类层次结构的完整性。

5、可以跨越类层次结构,访问不同层次的元素类,做出相应的操作。

缺点:

1、增加新的元素会非常困难。

2、实现起来比较复杂,会增加系统的复杂性。

3、破坏封装,如果将访问行为放在各个元素中,则可以不暴露元素的内部结构和状态,但使用访问者模式的时候,为了让访问者能获取到所关心的信息,元素类不得不暴露出一些内部的状态和结构,就像收入和支出类必须提供访问金额和单子的项目的方法一样。

适用性:

1、数据结构稳定,作用于数据结构的操作经常变化的时候。

2、当一个数据结构中,一些元素类需要负责与其不相关的操作的时候,为了将这些操作分离出去,以减少这些元素类的职责时,可以使用访问者模式。

3、有时在对数据结构上的元素进行操作的时候,需要区分具体的类型,这时使用访问者模式可以针对不同的类型,在访问者类中定义不同的操作,从而去除掉类型判断。

【Java设计模式】:行为型模式—访问者模式相关推荐

  1. 【Java设计模式】简单学访问者模式——我的选择是,Yes

    目录 说明 实现方式 优点 缺点 应用场景 其他链接 说明 行为型模式之一,其他还有命令模式.模板方法模式.迭代器模式.观察者模式.中介者模式.备忘录模式.解释器模式(Interpreter模式).状 ...

  2. java设计模式——创建型之建造者模式

    自大学课程初识设计模式以来,就越发觉得有必要系统学习一下设计模式. 刚好在实习前准备期间课比较少,抽出一点时间整理一下记一些笔记,复制粘贴比较多. 笔记比较适合学习过设计模式的同学. Builder ...

  3. Java设计模式之行为型:访问者模式

    背景: 去医院看病时,医生会给你一个处方单要你去拿药,拿药我们可以分为两步走: (1)去柜台交钱,划价人员会根据处方单上的药进行划价,交钱. (2)去药房拿药,药房工作者同样根据处方单给你相对应的药. ...

  4. 【Java设计模式】简单学解释器模式——加减乘除

    目录 说明 实现方式 应用场景 其他链接 说明 行为型模式之一,其他还有命令模式.模板方法模式.访问者模式.观察者模式.中介者模式.备忘录模式.迭代器模式.状态模式.策略模式.职责链模式(责任链模式) ...

  5. 设计模式(创建型)之建造者模式(Builder Pattern)

    PS一句:最终还是选择CSDN来整理发表这几年的知识点,该文章平行迁移到CSDN.因为CSDN也支持MarkDown语法了,牛逼啊! [工匠若水 http://blog.csdn.net/yanbob ...

  6. Java 设计模式之静态工厂方法模式

    设计模式系列 创建型设计模式 Java 设计模式之单例模式 Java 设计模式之静态工厂方法模式 Java 设计模式之工厂方法模式 Java 设计模式之抽象工厂模式 Java 设计模式之Builder ...

  7. JAVA设计模式之3种工厂模式

    转自 JAVA设计模式之3种工厂模式 创建型模式 创建型模式(Creational Pattern)对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离.为了使软件的结构更加清晰,外 ...

  8. Java设计模式(16)中介模式(Mediator模式)

    Mediator定义:用一个中介对象来封装一系列关于对象交互行为. 为何使用Mediator模式/中介模式 各个对象之间的交互操作非常多,每个对象的行为操作都依赖彼此对方,修改一个对象的行为,同时会涉 ...

  9. Java设计模式(1)工厂模式(Factory模式)

    工厂模式定义:提供创建对象的接口. 为何使用工厂模式 工厂模式是我们最常用的模式了,著名的Jive论坛,就大量使用了工厂模式,工厂模式在Java程序系统可以说是随处可见. 为什么工厂模式是如此常用?因 ...

  10. Java设计模式(8)组合模式(Composite模式)

    Composite定义:将对象以树形结构组织起来,以达成"部分-整体" 的层次结构,使得客户端对单个对象和组合对象的使用具有一致性. Composite比较容易理解,想到Compo ...

最新文章

  1. 简单上手腾讯X5页面浏览
  2. 15款的视频处理软件免费下载
  3. epp是什么意思_什么是1K/2K/3K注塑?
  4. 数据结构之图的应用:有向无环图
  5. linux curl
  6. 常用adb shell命令大全
  7. ​​​​​​​国民经济行业分类(GB/T 4754—2017)
  8. 方差分析软件_手把手教你用Graphpad做单因素方差分析
  9. 乐高魔方机器人编程及图纸_乐高解魔方机器人
  10. 小学计算机键盘的初步认识教案,小学三年级信息技术--认识键盘教学设计(宋艳)[小编整理]...
  11. 应用时间序列分析第四章课后习题(R语言实现)
  12. java实现生命游戏
  13. 【TBSchedule】TBSchedule应用实战手册
  14. 01 excel 引用,数据引用,单元格引用
  15. 高性能高频RFID电子标签全向通道设备|图书馆防盗门HX-CH-TD6760性能与安装注意事项
  16. php word替换换行符,word换行替换
  17. dejavu-python的音频指纹识别库
  18. 心流:最优体验心理学-米哈里·契克森米哈赖
  19. 攻防世界Web:leaking
  20. Timestamp Encoding FEDformer_ Frequency Enhanced Decomposed Transformer for Long-term Series Forecas

热门文章

  1. android 循环输出字母,042 01 Android 零基础入门 01 Java基础语法 05 Java流程控制之循环结构 04 案例演示while循环的使用——循环输出英文字母...
  2. SDUT喵帕斯之天才算数少女
  3. python 读取gmail 邮箱消息
  4. linux系统时间不同步解决办法(同步本地时间)
  5. STM32实现红外感应传感器功能
  6. Lucid Motors 使用 Wolfspeed SiC 模块
  7. 以太坊的 Merkle 树
  8. Phonopy-Spectroscopy计算材料红外和Raman光谱
  9. android 分享wifi app下载安装,WiFi共享精灵移动版下载
  10. AJAX使用淘宝API查询手机归属地和运营商信息