在访问者模式(Visitor Pattern)中,我们使用了一个访问者类,它改变了元素类的执行算法。通过这种方式,元素的执行算法可以随着访问者改变而改变。这种类型的设计模式属于行为型模式。根据模式,元素对象已接受访问者对象,这样访问者对象就可以处理元素对象上的操作。
主要解决:
稳定的数据结构和易变的操作耦合问题
什么时候使用:
需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作”污染”这些对象的类,使用访问者模式将这些封装到类中。
访问者模式的简单例子
我们都知道财务都是有账本的,这个账本就可以作为一个对象结构,而它其中的元素有两种,收入和支出,这满足我们访问者模式的要求,即元素的个数是稳定的,因为账本中的元素只能是收入和支出。
而查看账本的人可能有这样几种,比如老板,会计事务所的注会,财务主管,等等。而这些人在看账本的时候显然目的和行为是不同的。
首先我们给出单子的接口相当于Element,它只有一个方法accept。
public interface Bill {
void accept(AccountBookViewer v);
}
其中的方法参数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;
}
@Override
public void accept(AccountBookViewer v) {
v.view(this);
}
public void OperationA(){
System.out.println("AAAAAAAAAAAAAAA");
}
/**
* @return the amount
*/
public double getAmount() {
return amount;
}
/**
* @param amount the amount to set
*/
public void setAmount(double amount) {
this.amount = amount;
}
/**
* @return the item
*/
public String getItem() {
return item;
}
/**
* @param item the item to set
*/
public void setItem(String item) {
this.item = 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;
}
@Override
public void accept(AccountBookViewer v) {
v.view(this);
}
/**
* @return the amount
*/
public double getAmount() {
return amount;
}
/**
* @param amount the amount to set
*/
public void setAmount(double amount) {
this.amount = amount;
}
/**
* @return the item
*/
public String getItem() {
return item;
}
/**
* @param item the item to set
*/
public void setItem(String item) {
this.item = item;
}
}
接下来是账本访问者类
public abstract class AccountBookViewer {
//查看消费的单子
abstract void view(ConsumeBill bill);
//查看收入的单子
abstract void view(IncomeBill bill);
}
访问者的实现
老板类,老板只关注一共花了多少钱以及一共收入多少钱,其余并不关心
public class Boss extends AccountBookViewer {
private double totalIncome;
private double totalConsume;
@Override
void view(ConsumeBill bill) {
totalConsume += bill.getAmount();
}
@Override
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 extends AccountBookViewer {
//注会在看账本时,如果是支出,则如果支出是工资,则需要看应该交的税交了没
public void view(ConsumeBill bill) {
if (bill.getItem().equals("工资")) {
System.out.println("注会查看工资是否交个人所得税。");
}
}
//如果是收入,则所有的收入都要交税
public void view(IncomeBill bill) {
System.out.println("注会查看收入交税了没。");
}
}
账本类,它是当前访问者模式例子中的对象结构
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,代码会显得非常臃肿,而且非常难以扩展和维护。
bill.accept(viewer);这段代码真正的执行类是通过运行时两次确定的
分析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方法的版本,如此一来,就完成了动态双分派的过程。
动态分派与静态分派的区别就是,动态是在运行时才能确定,相当于多台,静态是在编译时候就能确定