访问者模式,是行为型设计模式之一。访问者模式是一种将数据操作与数据结构分离的设计模式,它可以算是 23 中设计模式中最复杂的一个,但它的使用频率并不是很高,大多数情况下,你并不需要使用访问者模式,但是当你一旦需要使用它时,那你就是需要使用它了。
访问者模式的基本想法是,软件系统中拥有一个由许多对象构成的、比较稳定的对象结构,这些对象的类都拥有一个 accept 方法用来接受访问者对象的访问。访问者是一个接口,它拥有一个 visit 方法,这个方法对访问到的对象结构中不同类型的元素做出不同的处理。在对象结构的一次访问过程中,我们遍历整个对象结构,对每一个元素都实施 accept 方法,在每一个元素的 accept 方法中会调用访问者的 visit 方法,从而使访问者得以处理对象结构的每一个元素,我们可以针对对象结构设计不同的访问者类来完成不同的操作,达到区别对待的效果。
定义及使用场景
定义:封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作。
可以对定义这么理解:有这么一个操作,它是作用于一些元素之上的,而这些元素属于某一个对象结构。同时这个操作是在不改变各元素类的前提下,在这个前提下定义新操作是访问者模式精髓中的精髓。
使用场景
- 对象结构比较稳定,但经常需要在此对象结构上定义新的操作。
- 需要对一个对象结构中的对象进行很多不同的且不相关的操作,而需要避免这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。
UML
- Visitor: 接口或者抽象类,它定义了对每一个元素(Element)访问的行为,它的参数就是可以访问的元素,它的方法数理论上来讲与元素个数是一样的,因此,访问者模式要求元素的类族要稳定,如果经常添加、移除元素类,必然会导致频繁地修改Visitor接口,如果这样则不适合使用访问者模式。
- ConcreteVisitor1、ConcreteVisitor2:具体的访问类,它需要给出对每一个元素类访问时所产生的具体行为。
- Element:元素接口或者抽象类,它定义了一个接受访问者的方法(Accept),其意义是指每一个元素都要可以被访问者访问。
- ConcreteElementA、ConcreteElementB:具体的元素类,它提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
- ObjectStructure:定义当中所说的对象结构,对象结构是一个抽象表述,它内部管理了元素集合,并且可以迭代这些元素供访问者访问。
访问者模式的简单例子
我们都知道财务都是有账本的,这个账本就可以作为一个对象结构,而它其中的元素有两种,收入和支出,这满足我们访问者模式的要求,即元素的个数是稳定的,因为账本中的元素只能是收入和支出。
而查看账本的人可能有这样几种,比如老板,会计事务所的注会,财务主管,等等。而这些人在看账本的时候显然目的和行为是不同的。
首先我们给出单子的接口,它只有一个方法accept。
package com.dsguo.visitor;
/**
* 单个单子的接口(相当于Element)
*/
public interface Bill {
void accept(AccountBookViewer viewer);
}
其中的方法参数AccountBookViewer是一个账本访问者接口,接下来也就是实现类,收入单子和消费单子,或者说收入和支出类。
package com.dsguo.visitor;
/**
* 消费单子
*/
public 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 viewer) {
viewer.viewConsumeBill(this);
}
public double getAmount() {
return amount;
}
public String getItem() {
return item;
}
}
package com.dsguo.visitor;
/**
* 收入单子
*/
public 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 viewer) {
viewer.viewIncomeBill(this);
}
public double getAmount() {
return amount;
}
public String getItem() {
return item;
}
}
上面最关键的还是里面的accept方法,它直接让访问者访问自己,这相当于一次静态分派
接下来是账本访问者接口
package com.dsguo.visitor;
/**
* 账单查看者接口(相当于Visitor)
*/
public interface AccountBookViewer {
/**
* 查看消费的单子
* @param bill
*/
void viewConsumeBill(ConsumeBill bill);
/**
* 查看收入的单子
* @param bill
*/
void viewIncomeBill(IncomeBill bill);
}
访问者的实现
package com.dsguo.visitor;
/**
* 老板类,查看账本的类之一
*/
public class Boss implements AccountBookViewer {
private double totalIncome;
private double totalConsume;
@Override
public void viewConsumeBill(ConsumeBill bill) {
totalConsume += bill.getAmount();
}
@Override
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;
}
}
package com.dsguo.visitor;
/**
* 注册会计师类,查看账本的类之一
*/
public class CPA implements AccountBookViewer {
/**
* 注会在看账本时,如果是支出,则如果支出是工资,则需要看应该交的税交了没
* @param bill
*/
@Override
public void viewConsumeBill(ConsumeBill bill) {
if (bill.getItem().equals("工资")) {
System.out.println("注册会计师查看工资是否交个人所得税。");
}
}
/**
* 如果是收入,则所有的收入都要交税
* @param bill
*/
@Override
public void viewIncomeBill(IncomeBill bill) {
System.out.println("注册会计师查看收入交税了没有");
}
}
老板只关心收入和支出的总额,而注会只关注该交税的是否交税
接下来是账本类,它是当前访问者模式例子中的对象结构
package com.dsguo.visitor;
import java.util.ArrayList;
import java.util.List;
/**
* 账本类(相当于ObjectStruture)
*/
public class AccountBook {
//单子列表
private List<Bill> bills = new ArrayList<Bill>();
/**
* 添加单子
* @param bill
*/
public void addBill(Bill bill) {
bills.add(bill);
}
/**
* 供账本的查看者查看账本
* @param viewer
*/
public void show(AccountBookViewer viewer) {
for (Bill bill: bills) {
bill.accept(viewer);
}
}
}
账本类当中有一个列表,这个列表是元素(Bill)的集合,这便是对象结构的通常表示,它一般会是一堆元素的集合,不过这个集合不一定是列表,也可能是树,链表等等任何数据结构,甚至是若干个数据结构。其中show方法,就是账本类的精髓,它会枚举每一个元素,让访问者访问。
测试客户端
package com.dsguo.visitor;
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();
}
}
运行结果:
注册会计师查看收入交税了没有
注册会计师查看收入交税了没有
注册会计师查看工资是否交个人所得税。
老板查看一共话费多少,数目:3000.0
老板查看一共收入多少,数目:22000.0
总结
优点
- 使得数据结构和作用于结构上的操作解耦,使得操作集合可以独立变化。
- 添加新的操作或者说访问者会非常容易。
- 将对各个元素的一组操作集中在一个访问者类当中。
- 使得类层次结构不改变的情况下,可以针对各个层次做出不同的操作,而不影响类层次结构的完整性。
- 可以跨越类层次结构,访问不同层次的元素类,做出相应的操作。
缺点
- 增加新的元素会非常困难。
- 实现起来比较复杂,会增加系统的复杂性。
- 破坏封装,如果将访问行为放在各个元素中,则可以不暴露元素的内部结构和状态,但使用访问者模式的时候,为了让访问者能获取到所关心的信息,元素类不得不暴露出一些内部的状态和结构,就像收入和支出类必须提供访问金额和单子的项目的方法一样。
适用性
- 数据结构稳定,作用于数据结构的操作经常变化的时候。
- 当一个数据结构中,一些元素类需要负责与其不相关的操作的时候,为了将这些操作分离出去,以减少这些元素类的职责时,可以使用访问者模式。
- 有时在对数据结构上的元素进行操作的时候,需要区分具体的类型,这时使用访问者模式可以针对不同的类型,在访问者类中定义不同的操作,从而去除掉类型判断。