访问者模式

定义

表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。

一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A {
public void method1(){
System.out.println("我是A");
}
public void method2(B b){
b.showA(this);
}
}
class B {
public void showA(A a){
a.method1();
}
}
public class Test {
public static void main(String[] args){
A a = new A();
a.method1();
a.method2(new B());
}
}

上面的运行结果是:

我是A

我是A

在上面的列子中,B是A的访问者。

模式解析

UML图

角色

  • Visitor接口:元素(Element)访问的行为的抽象接口,其参数是被访问的元素(Element),它的方法个数理论上来讲与元素个数(Element的实现类个数)是一样的,从这点不难看出,访问者模式要求元素类的个数不能改变(不能改变的意思是说,如果元素类的个数经常改变,则说明不适合使用访问者模式)。

  • ConcreteVisitor:具体的访问者实现,它需要给出对每一个元素类访问时所产生的具体行为。

  • Element接口:元素的接口,它定义了一个接受访问者(Visitor)的(accept)方法,其意义是指,每一个元素都要可以被访问者访问。

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

  • ObjectStructure:这个便是定义当中所提到的对象结构,对象结构是一个抽象表述,具体点可以理解为一个具有容器性质或者复合对象特性的类,它会含有一组元素(Element),并且可以迭代这些元素,供访问者访问。

在上面五个角色当中,最重要的就是最后一个ObjectStructure,所谓的访问者模式,就是为了让访问者可以方便的访问对象结构而存在的。

代码实现

Visitor接口

1
2
3
4
5
public interface Viewer {
public void visit(ConsumeBill able );
public void visit(IncomeBill able );
}

ConcreteVisitor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Boss implements Viewer{
private int acount = 0;
@Override
public void visit(ConsumeBill able) {
acount += able.getAcount();
}
@Override
public void visit(PurchaseBill able) {
// TODO Auto-generated method stub
acount -= able.getAcount();
}
public int getTotalAcount(){
return acount;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CPA implements Viewer{
private int tax = 0;
@Override
public void visit(ConsumeBill able) {
tax += able.getTax();
}
@Override
public void visit(PurchaseBill able) {
// TODO Auto-generated method stub
tax += able.getTax();
}
public int getTotalTax(){
return tax;
}
}

Element接口

1
2
3
4
5
6
7
8
9
public abstract class Bill {
protected int acount;
protected int tax;
public void accept(Viewer v);
}

ConcreteElement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ConsumeBill extends Bill {
ConsumeBill(int acount, int tax){
this.acount = acount;
this.tax = tax;
}
@Override
public void accept(View v) {
v.visit(this);
}
public int getAmount() {
return amount;
}
public int getTax() {
return tax;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class PurchaseBill extends Bill {
PurchaseBill(int acount, int tax){
this.acount = acount;
this.tax = tax;
}
@Override
public void accept(Visitor v) {
v.visit(this);
}
public int getAmount() {
return amount;
}
public int getTax() {
return tax;
}
}

ObjectStructure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class BillBook {
private List<Bill> bills = new ArrayList<Bill>();
//添加元素
public void addBill(Bill bill){
bills.add(bill);
}
//共访问者查看
public void show(Viewer viewer){
for (Bill bill : bills) {
bill.accept(viewer);
}
}
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Client {
public static void main(String[] args) {
BillBook billBook = new BillBook();
//添加两条账单
billBook.addBill(new ConsumeBill(10000, 100));
billBook.addBill(new ConsumeBill(12000, 150));
//添加两条进货
billBook.addBill(new PurchaseBill(5000, 50));
billBook.addBill(new PurchaseBill(7000, 70));
Viewer boss = new Boss();
Viewer cpa = new CPA();
//两个访问者分别访问账本
billBook.show(cpa);
billBook.show(boss);
int totalAcount = boss.getTotalConsume();
int totalTax = cpa.getTotalTax();
}
}

可以看到,两个访问者boss和cpa对账本的查看,行为是完全不同的,但是这正是访问者模式的意义所在,它其实是将访问者这部分逻辑独立出去,让其自生自灭。我们可以直观的去理解,上面的代码中,账本以及账本中的元素是非常稳定的,这些几乎不可能改变,而最容易改变的就是访问者这部分。

访问者模式最大的优点就是增加访问者非常容易,我们从代码上来看,如果要增加一个访问者,你只需要做一件事即可,那就是写一个类,实现Viewer接口,然后就可以直接调用BillBook的show方法去访问账本了。此外,它避免了代码中使用过多的if-else

如增加一个CEO的Viewer,关注的是总收入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CEO implements Viewer{
private int money = 0;
@Override
public void visit(ConsumeBill able) {
money += able.getAcount();
money -= able.getTax();
}
@Override
public void visit(PurchaseBill able) {
// TODO Auto-generated method stub
acount -= able.getAcount();
money -= able.getTax();
}
public int getTotalMoney(){
return money;
}
}

优点

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

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

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

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

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

缺点

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

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

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

适用场景

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

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

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

忠告

正如《设计模式》的作者GoF对访问者模式的描述:大多数情况下,你并需要使用访问者模式,但是当你一旦需要使用它时,那你就是真的需要它了。当然这只是针对真正的大牛而言。在现实情况下(至少是我所处的环境当中),很多人往往沉迷于设计模式,他们使用一种设计模式时,从来不去认真考虑所使用的模式是否适合这种场景,而往往只是想展示一下自己对面向对象设计的驾驭能力。编程时有这种心理,往往会发生滥用设计模式的情况。所以,在学习设计模式时,一定要理解模式的适用性。必须做到使用一种模式是因为了解它的优点,不使用一种模式是因为了解它的弊端;而不是使用一种模式是因为不了解它的弊端,不使用一种模式是因为不了解它的优点。