定义
- 封装一些作用于某种数据结构的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
- 在被访问的类里面加一个对外提供接待访问者的接口
- 需要对一个对象结构中的对象进行很多不同操作(这些操作彼此没有关联),同时需要避免让这些操作"污染"这些对象的类,可以选用访问者模式解决
访问者模式主要由这五个角色组成,
- 抽象访问者(Visitor)角色:声明了一个或者多个方法操作,形成所有的具体访问者角色必须实现的接口。
- 具体访问者(ConcreteVisitor)角色:实现抽象访问者所声明的接口,也就是抽象访问者所声明的各个访问操作。
- 抽象节点(Node)角色:声明一个接受操作,接受一个访问者对象作为一个参数。
- 具体节点(ConcreteNode)角色:实现了抽象节点所规定的接受操作。
- 结构对象(ObjectStructure)角色:有如下的责任,可以遍历结构中的所有元素。
优点
- 访问者模式符合单一职责原则、让程序具有优秀的扩展性、灵活性非常高
- 访问者模式可以对功能进行统一,可以做报表、UI、拦截器与过滤器,适用于数据
结构相对稳定的系统 - 使得数据结构和作用于结构上的操作解耦,使得操作集合可以独立变化
- 添加新的操作或者说访问者会非常容易。
- 使得类层次结构不改变的情况下,可以针对各个层次做出不同的操作,而不影响类层次结构的完整性。
缺点
- 具体元素对访问者公布细节,也就是说访问者关注了其他类的内部细节,这是迪米
特法则所不建议的, 这样造成了具体元素变更比较困难 - 违背了依赖倒转原则。访问者依赖的是具体元素,而不是抽象元素
- 因此,如果一个系统有比较稳定的数据结构,又有经常变化的功能需求,那么访问
者模式就是比较合适的. - 实现起来比较复杂,会增加系统的复杂性。
- 增加新的元素会非常困难
- 破坏封装,如果将访问行为放在各个元素中,则可以不暴露元素的内部结构和状态,但使用访问者模式的时候,为了让访问者能获取到所关心的信息,元素类不得不暴露出一些内部的状态和结构,就像收入和支出类必须提供访问金额和单子的项目的方法一样
代码
- 按照账目简单例子
- 因为他只有两个元素,收入和支出,这满足我们要求的元素个数稳定
- 但是查看账本的人不同,比如老板,会计,财务主管等,他们的目的和行为是不同
package com.example.demo;
import java.util.ArrayList;
import java.util.List;
public class test12 {
public static void main(String[] args) {
AccountBook accountBook = new AccountBook();
//收入
accountBook.addBill(new IncomeBill(100, "卖商品"));
accountBook.addBill(new IncomeBill(10, "收房租"));
//支出
accountBook.addBill(new ConsumeBill(20, "工资"));
accountBook.addBill(new ConsumeBill(10, "水电费"));
AccountBookViewer boss = new Boss();
AccountBookViewer cpa =new CPA();
//两个访问者分别访问账本
accountBook.show(boss);
accountBook.show(cpa);
((Boss) boss).getTotalConsume();
((Boss) boss).getTotalIncome();
/* 收入交税了没。
收入交税了没。
多少支持是水电费。
老板查看一共支出多少,数目是:110.0
老板查看一共收入多少,数目是:30.0*/
}
}
//账单查看者接口 重载方法实现
interface AccountBookViewer {
//查看消费的单子
void view(ConsumeBill bill);
//查看收入的单子
void view(IncomeBill bill);
}
//单个单子的接口
interface Bill {
void accept(AccountBookViewer accountBookViewer);
}
//消费的单子
class ConsumeBill implements Bill {
private double amount;
private String item;
public ConsumeBill(double amount, String item) {
super();
this.amount = amount;
this.item = item;
}
@Override
public void accept(AccountBookViewer accountBookViewer) {
accountBookViewer.view(this);
}
public double getAmount() {
return amount;
}
public void setAmount(double amount) {
this.amount = amount;
}
public String getItem() {
return item;
}
public void setItem(String item) {
this.item = item;
}
}
//收入单子
class IncomeBill implements Bill {
private double amount;
private String item;
public IncomeBill(double amount, String item) {
super();
this.amount = amount;
this.item = item;
}
@Override
public void accept(AccountBookViewer accountBookViewer) {
accountBookViewer.view(this);
}
public double getAmount() {
return amount;
}
public void setAmount(double amount) {
this.amount = amount;
}
public String getItem() {
return item;
}
public void setItem(String item) {
this.item = item;
}
}
//-======================访问者==============================
class Boss implements AccountBookViewer {
private double totalConsume;
private double totalIncome;
//支出多少
@Override
public void view(ConsumeBill bill) {
totalConsume += bill.getAmount();
}
//收入多少
@Override
public void view(IncomeBill bill) {
totalIncome += bill.getAmount();
}
public double getTotalConsume() {
System.out.println("老板查看一共支出多少,数目是:" + totalIncome);
return totalConsume;
}
public void setTotalConsume(double totalConsume) {
this.totalConsume = totalConsume;
}
public double getTotalIncome() {
System.out.println("老板查看一共收入多少,数目是:" + totalConsume);
return totalIncome;
}
public void setTotalIncome(double totalIncome) {
this.totalIncome = totalIncome;
}
}
//注册会计师类,查看账本的类之一 访问者
class CPA implements AccountBookViewer {
//支出多少
@Override
public void view(ConsumeBill bill) {
if (bill.getItem().equals("水电费")) {
System.out.println("多少支持是水电费。");
}
}
//收入
@Override
public void view(IncomeBill bill) {
System.out.println("收入交税了没。");
}
}
//账本类(相当于ObjectStruture)
class AccountBook {
//单子列表
private List<Bill> billList = new ArrayList<>();
//添加单子
public void addBill(Bill bill) {
billList.add(bill);
}
public void show(AccountBookViewer viewer) {
for (Bill bill : billList) {
bill.accept(viewer);
}
}
}
代码分析
- 上面的代码中,可以这么理解,账本以及账本中的元素是非常稳定的,这些几乎不可能改变,而最容易改变的就是访问者这部分。
- 访问者模式最大的优点就是增加访问者非常容易,我们从代码上来看,如果要增加一个访问者,你只需要做一件事即可,那就是写一个类,实现AccountBookViewer接口,然后就可以直接调用AccountBook的show方法去访问账本了
- 如果没使用访问者模式,一定会增加许多if else,而且每增加一个访问者,你都需要改你的if else,代码会显得非常臃肿,而且非常难以扩展和维护。
静态分派以及动态分派
- 变量被声明时的类型叫做变量的静态类型(Static Type),有些人又把静态类型叫做明显类型(Apparent Type);而变量所引用的对象的真实类型又叫做变量的实际类型(Actual Type)。比如:
Map map =null; 静态类型(也叫明显类型)是Map
map = new HashMap<>(); 实际类型是HashMap
静态分派
- 静态分派(Static Dispatch)发生在编译时期,分派根据静态类型信息发生。
- 在静态分派判断的时候,我们根据多个判断依据(即参数类型和个数)判断出了方法的版本,那么这个就是多分派的概念,因为我们有一个以上的考量标准,也可以称为宗量。所以JAVA是静态多分派的语言。
public static void main(String[] args) {
test();
test("小黄");
}
public static void test(){
System.out.println(" 空 ");
}
public static void test(String name ){
System.out.println(name+" 空 ");
}
动态分派
- 对于动态分派,与静态相反,它不是在编译期确定的方法版本,而是在运行时才能确定。而动态分派最典型的应用就是多态的特性
- 这段程序输出结果为依次打印男人和女人,然而这里的test方法版本,就无法根据man和woman的静态类型去判断了,他们的静态类型都是Person接口,根本无从判断。
- 显然,产生的输出结果,就是因为test方法的版本是在运行时判断的,这就是动态分派。
interface Person {
void test();
}
static class Man implements Person {
public void test() {
System.out.println("男人");
}
}
static class Woman implements Person {
public void test() {
System.out.println("女人");
}
}
public static void main(String[] args) {
Person man = new Man();
Person woman = new Woman();
man.test();
woman.test();
}
访问者模式中的伪动态双分派
- 访问者模式中使用的是伪动态双分派,所谓的动态双分派就是在运行时依据两个实际类型去判断一个方法的运行行为,而访问者模式实现的手段是进行了两次动态单分派来达到这个效果。
- 回到上面例子当中账本类中的accept方法
for (Bill bill : billList) {
bill.accept(viewer);
}
- 这里就是依据biil和viewer两个实际类型决定了view方法的版本,从而决定了accept方法的动作。
- 分析accept方法的调用过程
-
当调用accept方法时,根据bill的实际类型决定是调用ConsumeBill还是IncomeBill的accept方法。
-
这时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的类型不是动态确定的,你写在哪个类当中,它的静态类型就是哪个类,这是在编译期就确定的,不确定的是它的实际类型,请各位区分开这一点。
进阶版访问者
- 假设我们上面的例子当中再添加一个财务主管,而财务主管不管你是支出还是收入,都要详细的查看你的单子的项目以及金额,简单点说就是财务主管类的两个view方法的代码是一样的。
- 这里的将两个view方法抽取的方案是,我们可以将元素提炼出层次结构,针对层次结构提供操作的方法,这样就实现了优点当中最后两点提到的针对层次定义操作以及跨越层次定义操作。
import java.util.ArrayList;
import java.util.List;
public class test12 {
public static void main(String[] args) {
AccountBook accountBook = new AccountBook ();
//收入
accountBook.addBill(new IncomeBill(100, "卖商品"));
accountBook.addBill(new IncomeBill(10, "收房租"));
//支出
accountBook.addBill(new ConsumeBill(20, "工资"));
accountBook.addBill(new ConsumeBill(10, "水电费"));
Viewer boss = new Boss();
Viewer cpa =new CPA();
Viewer cfo =new CFO();
//两个访问者分别访问账本
accountBook.show(boss);
accountBook.show(cpa);
accountBook.show(cfo);
((Boss) boss).getTotalConsume();
((Boss) boss).getTotalIncome();
/* 财务主管查看账本时,每一个都核对项目和金额,金额是100.0,项目是卖商品
财务主管查看账本时,每一个都核对项目和金额,金额是10.0,项目是收房租
财务主管查看账本时,每一个都核对项目和金额,金额是20.0,项目是工资
财务主管查看账本时,每一个都核对项目和金额,金额是10.0,项目是水电费
老板查看一共花费多少,数目是:0.0
老板查看一共收入多少,数目是:0.0*/
}
}
//单个单子的接口(相当于Element)
interface Bill {
void accept(Viewer viewer);
}
//抽象单子类,一个高层次的单子抽象
abstract class AbstractBill implements Bill{
protected double amount;
protected String item;
public AbstractBill(double amount,String item){
super();
this.amount = amount;
this.item = item;
}
public double getAmount() {
return amount;
}
public String getItem() {
return item;
}
}
//收入单子
class IncomeBill extends AbstractBill{
public IncomeBill(double amout,String item){
super(amout,item);
}
@Override
public void accept(Viewer viewer) {
if(viewer instanceof AbstractViewer){
viewer.viewAbstractBill(this);
return ;
}
viewer.viewAbstractBill(this);
}
}
//消费单子
class ConsumeBill extends AbstractBill{
public ConsumeBill (double amout,String item){
super(amout,item);
}
@Override
public void accept(Viewer viewer) {
if(viewer instanceof AbstractViewer){
viewer.viewAbstractBill(this);
return ;
}
viewer.viewAbstractBill(this);
}
}
//超级访问者接口(它支持定义高层操作)
interface Viewer{
void viewAbstractBill(AbstractBill bill);
}
//比Viewer接口低一个层次的访问者接口
abstract class AbstractViewer implements Viewer{
//查看消费的单子
abstract void viewConsumeBill(ConsumeBill bill);
//查看收入的单子
abstract void viewIncomeBill(IncomeBill bill);
public final void viewAbstractBill(AbstractBill bill){}
}
//老板类,查看账本的类之一,作用于最低层次结构
class Boss extends AbstractViewer{
private double totalIncome;
private double totalConsume;
//老板只关注一共花了多少钱以及一共收入多少钱,其余并不关心
public void viewConsumeBill(ConsumeBill bill) {
totalConsume += bill.getAmount();
}
public void viewIncomeBill(IncomeBill bill) {
totalIncome += bill.getAmount();
}
public double getTotalIncome() {
System.out.println("老板查看一共收入多少,数目是:" + totalIncome);
return totalIncome;
}
public double getTotalConsume() {
System.out.println("老板查看一共花费多少,数目是:" + totalConsume);
return totalConsume;
}
}
//注册会计师类,查看账本的类之一,作用于最低层次结构
class CPA extends AbstractViewer{
//注会在看账本时,如果是支出,则如果支出是工资,则需要看应该交的税交了没
public void viewConsumeBill(ConsumeBill bill) {
if (bill.getItem().equals("工资")) {
System.out.println("注会查看是否交个人所得税。");
}
}
//如果是收入,则所有的收入都要交税
public void viewIncomeBill(IncomeBill bill) {
System.out.println("注会查看收入交税了没。");
}
}
//财务主管类,查看账本的类之一,作用于高层的层次结构
class CFO implements Viewer {
//财务主管对每一个单子都要核对项目和金额
public void viewAbstractBill(AbstractBill bill) {
System.out.println("财务主管查看账本时,每一个都核对项目和金额,金额是" + bill.getAmount() + ",项目是" + bill.getItem());
}
}
//账本类(相当于ObjectStruture)
class AccountBook {
//单子列表
private List<Bill> billList = new ArrayList<Bill>();
//添加单子
public void addBill(Bill bill) {
billList.add(bill);
}
//供账本的查看者查看账本
public void show(Viewer viewer) {
for (Bill bill : billList) {
bill.accept(viewer);
}
}
}
回想一下,要是再出现和财务主管一样对所有单子都是一样操作的人,我们就不需要复制代码了,只需要让他实现Viewer接口就可以了,而如果要像老板和注会一样区分单子的具体类型,则继承AbstractViewer就可以。