定义:
封装某些作用于某种数据结构中各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
模式中的角色:
- 抽象访问者:抽象类或者接口,声明访问者可以访问哪些元素,具体到程序中就是visit方法中的参数定义哪些对象是可以被访问的。
- 访问者:实现抽象访问者所声明的方法,它影响到访问者访问到一个类后该干什么,要做什么事情。
- 抽象元素类:接口或者抽象类,声明接受哪一类访问者访问,程序上是通过accept方法中的参数来定义的。抽象元素一般有两类方法,一部分是本身的业务逻辑,另外就是允许接收哪类访问者来访问。
- 元素类:实现抽象元素类所声明的accept方法,通常都是visitor.visit(this),基本上已经形成一种定式了。
- 结构对象:一个元素的容器,一般包含一个容纳多个不同类、不同接口的容器,如List、Set、Map等,在项目中一般很少抽象出这个角色。
角色关系UML:
老板和会计查看账本的收入与支出
UML图如下:
java代码:
抽象元素
package demo29;
/**
*
* @ClassName: Bill
* @Description:抽象账单
* @author cheng
* @date 2017-8-30 下午04:57:33
*/
public interface Bill {
/**
*
* @Title: accept
* @Description:接收的访问者
* @param viewer
*/
void accept(AccountBookViewer viewer);
}
具体元素
package demo29;
/**
*
* @ClassName: ConsumeBill
* @Description:消费账单
* @author cheng
* @date 2017-8-30 下午05:01:25
*/
public class ConsumeBill implements Bill {
private double amount;// 消费金额
private String item;// 消费类别
/**
* 构造函数
*
* @param amount
* @param item
*/
public ConsumeBill(double amount, String item) {
super();
this.amount = amount;
this.item = item;
}
/**
* 复写
*/
public void accept(AccountBookViewer viewer) {
viewer.viewConsumeBill(this);
}
/**
*
* @Title: getAmount
* @Description: 获取消费账单金额
* @return
*/
public double getAmount() {
return amount;
}
public String getItem() {
return item;
}
}
package demo29;
/**
*
* @ClassName: IncomeBill
* @Description:收入账单
* @author cheng
* @date 2017-8-30 下午05:03:56
*/
public class IncomeBill implements Bill {
private double amount;// 收入金额
private String item;// 收入类别
/**
* 构造函数
*
* @param amount
* @param item
*/
public IncomeBill(double amount, String item) {
super();
this.amount = amount;
this.item = item;
}
/**
* 复写
*/
public void accept(AccountBookViewer viewer) {
viewer.viewIncomeBill(this);
}
/**
*
* @Title: getAmount
* @Description: 获取收入账单金额
* @return
*/
public double getAmount() {
return amount;
}
public String getItem() {
return item;
}
}
抽象访问者
package demo29;
/**
*
* @ClassName: AccountBookViewer
* @Description:抽象访问者
* @author cheng
* @date 2017-8-30 下午04:59:29
*/
public interface AccountBookViewer {
/**
*
* @Title: viewConsumeBill
* @Description: 查看消费的账单
* @param bill
*/
void viewConsumeBill(ConsumeBill bill);
/**
*
* @Title: viewIncomeBill
* @Description: 查看收入的账单
* @param bill
*/
void viewIncomeBill(IncomeBill bill);
}
具体访问者
package demo29;
/**
*
* @ClassName: Boss
* @Description: 老板类,查看账本的类之一
* @author cheng
* @date 2017-8-30 下午05:05:37
*/
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 demo29;
/**
*
* @ClassName: CPA
* @Description: 注册会计师类,查看账本的类之一
* @author chengrui
* @date 2017-8-30 下午05:07:09
*/
public class CPA implements AccountBookViewer {
/**
* 注册会计师在看账本时,如果是支出,则如果支出是工资,则需要看应该交的税交了没
*/
@Override
public void viewConsumeBill(ConsumeBill bill) {
if (bill.getItem().equals("工资")) {
System.out.println("注册会计师查看工资是否交个人所得税。");
}
}
/**
* 如果是收入,则所有的收入都要交税
*/
@Override
public void viewIncomeBill(IncomeBill bill) {
System.out.println("注册会计师查看收入交税了没。");
}
}
结构对象
package demo29;
import java.util.ArrayList;
import java.util.List;
/**
*
* @ClassName: AccountBook
* @Description:账本
* @author cheng
* @date 2017-8-30 下午05:09:25
*/
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);
}
}
}
测试
package demo29;
/**
*
* @ClassName: ClientTest
* @Description:测试
* @author cheng
* @date 2017-8-30 下午05:10:26
*/
public class ClientTest {
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();
}
}
运行结果
特点
- 访问者模式把数据结构和作用于结构上的操作解耦合,使得操作集合可相对自由地演化。
- 访问者模式适用于数据结构相对稳定算法又易变化的系统。因为访问者模式使得算法操作增加变得容易。若系统数据结构对象易于变化,经常有新的数据对象增加进来,则不适合使用访问者模式。
优点
- 符合单一职责原则:凡是适用访问者模式的场景中,元素类中需要封装在访问者中的操作必定是与元素类本身关系不大且是易变的操作,使用访问者模式一方面符合单一职责原则,另一方面,因为被封装的操作通常来说都是易变的,所以当发生变化时,就可以在不改变元素类本身的前提下,实现对变化部分的扩展。
- 扩展性良好:元素类可以通过接受不同的访问者来实现对不同操作的扩展。
缺点
但是,访问者模式并不是那么完美,它也有着致命的缺陷:
增加新的元素类比较困难。
通过访问者模式的代码可以看到,在访问者类中,每一个元素类都有它对应的处理方法,也就是说,每增加一个元素类都需要修改访问者类(也包括访问者类的子类或者实现类),修改起来相当麻烦。也就是说,在元素类数目不确定的情况下,应该慎用访问者模式。所以,访问者模式比较适用于对已有功能的重构,比如说,一个项目的基本功能已经确定下来,元素类的数据已经基本确定下来不会变了,会变的只是这些元素内的相关操作,这时候,我们可以使用访问者模式对原有的代码进行重构一遍,这样一来,就可以在不修改各个元素类的情况下,对原有功能进行修改。
适用场景
假如一个对象中存在着一些与本对象不相干(或者关系较弱)的操作,为了避免这些操作污染这个对象,则可以使用访问者模式来把这些操作封装到访问者中去。
假如一组对象中,存在着相似的操作,为了避免出现大量重复的代码,也可以将这些重复的操作封装到访问者中去。
一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作。
需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而你想避免让这些操作“污染”这些对象的类。Visitor模式使得你可以将相关的操作集中起来 定义在一个类中。
当该对象结构被很多应用共享时,用Visitor模式让每个应用仅包含需要用到的操作。
定义对象结构的类很少改变,但经常需要在此结构上定义新的操作。改变对象结构类需要重定义对所有访问者的接口,这可能需要很大的代价。如果对象结构类经常改变,那么可能还是在这些类中定义这些操作较好。
总结
正如《设计模式》的作者GoF对访问者模式的描述:大多数情况下,你并需要使用访问者模式,但是当你一旦需要使用它时,那你就是真的需要它了。当然这只是针对真正的大牛而言。在现实情况下(至少是我所处的环境当中),很多人往往沉迷于设计模式,他们使用一种设计模式时,从来不去认真考虑所使用的模式是否适合这种场景,而往往只是想展示一下自己对面向对象设计的驾驭能力。编程时有这种心理,往往会发生滥用设计模式的情况。所以,在学习设计模式时,一定要理解模式的适用性。必须做到使用一种模式是因为了解它的优点,不使用一种模式是因为了解它的弊端;而不是使用一种模式是因为不了解它的弊端,不使用一种模式是因为不了解它的优点。