目录

行为型_访问者模式

原文:https://www.cnblogs.com/adamjwh/p/11018268.html

在现实生活中,有些集合对象中存在多种不同的元素,且每种元素也存在多种不同的访问者和处理方式。例如,公园中存在多个景点,也存在多个游客,不同的游客对同一个景点的评价可能不同;医院医生开的处方单中包含多种药元素,査看它的划价员和药房工作人员对它的处理方式也不同,划价员根据处方单上面的药品名和数量进行划价,药房工作人员根据处方单的内容进行抓药。

这样的例子还有很多,例如,电影或电视剧中的人物角色,不同的观众对他们的评价也不同;还有顾客在商场购物时放在“购物车”中的商品,顾客主要关心所选商品的性价比,而收银员关心的是商品的价格和数量。

这些被处理的数据元素相对稳定而访问方式多种多样的数据结构,如果用“访问者模式”来处理比较方便。访问者模式能把处理方法从数据结构中分离出来,并可以根据需要增加新的处理方法,且不用修改原来的程序代码与数据结构,这提高了程序的扩展性和灵活性。

访问者模式的基本想法是,软件系统中拥有一个由许多对象构成的、比较稳定的对象结构,这些对象的类都拥有一个 accept 方法用来接受访问者对象的访问。访问者是一个接口,它拥有一个 visit 方法,这个方法对访问到的对象结构中不同类型的元素做出不同的处理。在对象结构的一次访问过程中,我们遍历整个对象结构,对每一个元素都实施 accept 方法,在每一个元素的 accept 方法中会调用访问者的 visit 方法,从而使访问者得以处理对象结构的每一个元素,我们可以针对对象结构设计不同的访问者类来完成不同的操作,达到区别对待的效果。

定义与特点

访问者(Visitor)模式的定义:将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构进行分离,是行为类模式中最复杂的一种模式。

访问者(Visitor)模式是一种对象行为型模式,其主要优点如下。

  1. 扩展性好。能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
  2. 复用性好。可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。
  3. 灵活性好。访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。
  4. 符合单一职责原则。访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。

访问者(Visitor)模式的主要缺点如下。

  1. 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
  2. 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。
  3. 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。

UML图

https://gitee.com/lienhui68/picStore/raw/master/null/20200816222958.png

  • Visitor

    接口或者抽象类,它定义了对每一个元素(Element)访问的行为,它的参数就是可以访问的元素,它的方法数理论上来讲与元素个数是一样的,因此,访问者模式要求元素的类族要稳定,如果经常添加、移除元素类,必然会导致频繁地修改Visitor接口,如果这样则不适合使用访问者模式。

  • ConcreteVisitor

    具体的访问类,它需要给出对每一个元素类访问时所产生的具体行为

  • Element

    元素接口或者抽象类,它定义了一个接受访问者的方法(Accept),其意义是指每一个元素都要可以被访问者访问。

  • ConcreteElement

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

  • ObjectStructure

    定义当中所说的对象结构,对象结构是一个抽象表述,它内部管理了元素集合,并且可以迭代这些元素供访问者访问。

简单案例

我们都知道财务都是有账本的,这个账本就可以作为一个对象结构,而它其中的元素有两种,收入账单和支出账单,这满足我们访问者模式的要求,即元素的个数是稳定的,因为账本中的元素只能是收入账单和支出账单。

而查看账本的人可能有这样几种,比如老板,会计事务所的注会,财务主管,等等。而这些人在看账本的时候显然目的和行为是不同的。

首先我们给出账单的接口,它只有一个方法accept。

1
2
3
4
// 账单,相当于Element
interface Bill {
    void accept(AccountBookViewer viewer);
}

其中的方法参数AccountBookViewer是一个账本访问者接口,接下来也就是实现类,收入账单和消费账单,或者说收入和支出类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 收入账单,相当于ConcreteElement
@Getter
@AllArgsConstructor
class IncomeBill implements Bill {

    // 收入金额
    private double amount;

    // 款项
    private String item;

    @Override
    public void accept(AccountBookViewer viewer) {
        viewer.view(this);
    }
}

// 支出账单,相当于ConcreteElement
@Getter
@AllArgsConstructor
class ConsumeBill implements Bill {

    // 支出金额
    private double amount;

    // 款项
    private String item;

    @Override
    public void accept(AccountBookViewer viewer) {
        viewer.view(this);
    }
}

上面最关键的还是里面的accept方法,它直接让访问者访问自己,这相当于一次静态分派,当然我们也可以不使用重载而直接给方法不同的名称。

双分派的精髓就是这个this对象

接下来是账本访问者接口

1
2
3
4
5
6
// 账本访问者,相当于Visitor
interface AccountBookViewer {
    void view(IncomeBill bill);

    void view(ConsumeBill bill);
}

这两个方法是重载方法,就是在上面的元素类当中用到的,当然你也可以按照访问者模式类图当中的方式去做,将两个方法分别命名为viewConsumeBill和viewIncomeBill,而一般建议按照类图上来做的

访问者的实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 老板,相当于ConcreteVisitor
@Getter
class Boss implements AccountBookViewer {

    // 总收入
    private double totalIncome;
    // 总支出
    private double totalConsume;

    @Override
    public void view(IncomeBill bill) {
        this.totalIncome += bill.getAmount();
    }

    @Override
    public void view(ConsumeBill bill) {
        this.totalConsume += bill.getAmount();
    }

    public double getTotalIncome() {
        System.out.println("老板查看一共收入多少,数目是:" + totalIncome);
        return totalIncome;
    }

    public double getTotalConsume() {
        System.out.println("老板查看一共花费多少,数目是:" + totalConsume);
        return totalConsume;
    }
}

// 注会,相当于ConcreteVisitor
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("注会查看收入交税了没。");
    }
}

老板只关心收入和支出的总额,而注会只关注该交税的是否交税。接下来是账本类,它是当前访问者模式例子中的对象结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class AccountBook {
    // 账单列表
    private List<Bill> bills = Lists.newArrayList();

    // 添加单子
    public void addBill(Bill bill) {
        bills.add(bill);
    }

    // 供张本的查看者查看张本
    public void show(AccountBookViewer viewer) {
        bills.forEach(bill -> bill.accept(viewer));
    }
}

账本类当中有一个列表,这个列表是元素(Bill)的集合,这便是对象结构的通常表示,它一般会是一堆元素的集合,不过这个集合不一定是列表,也可能是树,链表等等任何数据结构,甚至是若干个数据结构。其中的show方法,就是账本类的精髓,它会枚举每一个元素,让访问者访问。

测试客户端

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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

上面的代码中,可以这么理解,账本以及账本中的元素类型是非常稳定的,这些几乎不可能改变,而最容易改变的就是访问者这部分。

访问者模式最大的优点就是增加访问者非常容易,我们从代码上来看,如果要增加一个访问者,你只需要做一件事即可,那就是写一个类,实现AccountBookViewer接口,然后就可以直接调用AccountBook的show方法去访问账本了。

如果没使用访问者模式,一定会增加许多if else,而且每增加一个访问者,你都需要改你的if else,代码会显得非常臃肿,而且非常难以扩展和维护。

访问者模式中的伪动态双分派

访问者模式中使用的是伪动态双分派所谓的动态双分派就是在运行时依据两个实际类型去确定一个方法的执行版本而访问者模式实现的手段是进行了两次动态单分派来达到这个效果。

回到上面例子当中账本类中的accept方法

1
bills.forEach(bill -> bill.accept(viewer));

这里就是依据biil和viewer两个实际类型决定了view方法的执行版本。

分析accept方法的调用过程

  • 当调用accept方法时,根据bill的实际类型决定是调用ConsumeBill还是IncomeBill的accept方法。

  • 这时accept方法的版本已经确定,假如是ConsumeBill,它的accept方法是调用下面这行代码。

    1
    2
    3
    4
    
    @Override
    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方法抽取的方案是,我们可以将元素提炼出层次结构,针对层次结构提供操作的方法,这样就实现了优点当中最后两点提到的针对层次定义操作以及跨越层次定义操作。

元素类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 账单,相当于Element
interface Bill {
    void accept(AccountBookViewer viewer);
}

@Getter
@AllArgsConstructor
abstract class AbstractBill implements Bill {
    // 收入金额
    private double amount;

    // 款项
    private String item;
}

// 收入账单,相当于ConcreteElement
@Getter
class IncomeBill extends AbstractBill {

    public IncomeBill(double amount, String item) {
        super(amount, item);
    }

    @Override
    public void accept(AccountBookViewer viewer) {
        if (viewer instanceof AbstractViewer) {
            ((AbstractViewer) viewer).viewIncomeBill(this);
            return;
        }
        viewer.viewAbstractBill(this);
    }
}


// 支出账单,相当于ConcreteElement
@Getter
class ConsumeBill extends AbstractBill {

    public ConsumeBill(double amount, String item) {
        super(amount, item);
    }

    @Override
    public void accept(AccountBookViewer viewer) {
        if (viewer instanceof AbstractViewer) {
            ((AbstractViewer) viewer).viewConsumeBill(this);
            return;
        }
        viewer.viewAbstractBill(this);
    }
}

这是元素类的层次结构,可以看到,我们的accept当中出现了if判断,这里的判断是在判断一个层次,这段代码是不会被更改的。

访问者层次

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// 账本访问者,相当于Visitor
interface AccountBookViewer {
    void viewAbstractBill(AbstractBill bill);
}

abstract class AbstractViewer implements AccountBookViewer {
    //查看消费的单子
    abstract void viewConsumeBill(ConsumeBill bill);

    //查看收入的单子
    abstract void viewIncomeBill(IncomeBill bill);

    public final void viewAbstractBill(AbstractBill bill) {
    }
}

// 老板,相当于ConcreteVisitor
@Getter
class Boss extends AbstractViewer {

    // 总收入
    private double totalIncome;
    // 总支出
    private double totalConsume;

    @Override
    public void viewIncomeBill(IncomeBill bill) {
        this.totalIncome += bill.getAmount();
    }

    @Override
    public void viewConsumeBill(ConsumeBill bill) {
        this.totalConsume += bill.getAmount();
    }

    public double getTotalIncome() {
        System.out.println("老板查看一共收入多少,数目是:" + totalIncome);
        return totalIncome;
    }

    public double getTotalConsume() {
        System.out.println("老板查看一共花费多少,数目是:" + totalConsume);
        return totalConsume;
    }
}

// 注会,相当于ConcreteVisitor
class CPA extends AbstractViewer {

    //注会在看账本时,如果是支出,则如果支出款项是工资,则需要看应该交的税交了没否则什么都不做
    @Override
    public void viewConsumeBill(ConsumeBill bill) {
        if (bill.getItem().equals("工资")) {
            System.out.println("注会查看工资是否交个人所得税。");
        }
    }

    //如果是收入,则所有的收入都要交税
    @Override
    public void viewIncomeBill(IncomeBill bill) {
        System.out.println("注会查看收入交税了没。");
    }
}

class AccountBook {
    // 账单列表
    private List<Bill> bills = Lists.newArrayList();

    // 添加单子
    public void addBill(Bill bill) {
        bills.add(bill);
    }

    // 供张本的查看者查看张本
    public void show(AccountBookViewer viewer) {
        bills.forEach(bill -> bill.accept(viewer));
    }

}

//财务主管类,查看账本的类之一,作用于高层的层次结构
class CFO implements AccountBookViewer {

    //财务主管对每一个单子都要核对项目和金额
    public void viewAbstractBill(AbstractBill bill) {
        System.out.println("财务主管查看账本时,每一个都核对项目和金额,金额是" + bill.getAmount() + ",项目是" + bill.getItem());
    }

}

财务主管(CFO)是针对AbstractBill这一层定义的操作,而原来的老板(Boss)和注册会计师(CPA)都是针对ConsumeBill和IncomeBill这一层定义的操作,这时已经产生了跨越层次结构的行为,老板和注册会计师都跨过了抽象单子这一层,直接针对具体的单子定义操作。

账本类没有变化,最后看客户端的使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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();
        AccountBookViewer cfo = new CFO();


        //两个访问者分别访问账本
        accountBook.show(cpa);
        accountBook.show(boss);
        accountBook.show(cfo);


        ((Boss) boss).getTotalConsume();
        ((Boss) boss).getTotalIncome();
    }
}
// 运行结果
注会查看收入交税了没
注会查看收入交税了没
注会查看工资是否交个人所得税
财务主管查看账本时每一个都核对项目和金额金额是10000.0项目是卖商品
财务主管查看账本时每一个都核对项目和金额金额是12000.0项目是卖广告位
财务主管查看账本时每一个都核对项目和金额金额是1000.0项目是工资
财务主管查看账本时每一个都核对项目和金额金额是2000.0项目是材料费
老板查看一共花费多少数目是3000.0
老板查看一共收入多少数目是22000.0

回想一下,要是再出现和财务主管一样对所有单子都是一样操作的人,我们就不需要复制代码了,只需要让他实现Viewer接口就可以了,而如果要像老板和注会一样区分单子的具体类型,则继承AbstractViewer就可以。

使用场景

通常在以下情况可以考虑使用访问者(Visitor)模式。

  1. 对象结构相对稳定,但其操作算法经常变化的程序。
  2. 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。
  3. 对象结构包含很多类型的对象,希望对这些对象实施一些依赖于其具体类型的操作。

模式扩展

访问者(Visitor)模式是使用频率较高的一种设计模式,它常常同以下两种设计模式联用。

  1. 与“迭代器模式”联用。因为访问者模式中的“对象结构”是一个包含元素角色的容器,当访问者遍历容器中的所有元素时,常常要用迭代器。如【例1】中的对象结构是用 List 实现的,它通过 List 对象的 Itemtor() 方法获取迭代器。如果对象结构中的聚合类没有提供迭代器,也可以用迭代器模式自定义一个。

  2. 访问者(Visitor)模式同“组合模式”联用。因为访问者(Visitor)模式中的“元素对象”可能是叶子对象或者是容器对象,如果元素对象包含容器对象,就必须用到组合模式,其结构图如图 4 所示。

    https://gitee.com/lienhui68/picStore/raw/master/null/20200826061150.png

总结

访问者模式把数据结构和作用于结构上的操作解耦合,使得操作集合可相对自由地演化。访问者模式适用于数据结构相对稳定算法又易变化的系统。因为访问者模式使得算法操作增加变得容易。若系统数据结构对象易于变化,经常有新的数据对象增加进来,则不适合使用访问者模式。