设计模式之访问者模式

访问者模式

访问者模式( Visitor Pattern)是一种将数据结构与数据操作分离的设计模式,是指封装作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作,属于行为型模式。

访问者模式被称为最复杂的设计模式,并且使用频率不高,设计模式的作者也评价为:大多情况下,你不需要使用访问者模式,但是一旦需要使用它时,那就真的需要使用了。访问者模式的基本思想是,针对系统中拥有固定类型数的对象结构(元素),在其内提供一个accept()方法用来接受访问者对象的访问。不同的访问者对同一元素的访问内容不同,使得相同的元素集合可以产生不同的数据结果。 accept()方法可以接收不同的访问者对象然后在内部将自己(元素)转发到接收到的访问者对象的 visit()方法内。访问者内部对应类型的visit()方法就会得到回调执行,对元素进行操作。也就是通过两次动态分发(第一次是对访问者的分发 accept()方法第二次是对元素的分发 visit()方法),オ最终将一个具体的元素传递到一个具体的访问者,如此一来,就解耦了数据结构与操作,且数据操作不会改变元素状态。

访问者模式的核心是,解耦数据结构与数据操作,使得对元素的操作具备优秀的扩展性。可以通过扩展不同的数据操作类型(访问者)实现对相同元素集的不同的操作。

通用UML图如下

image-20210501235119471

从UML类图中,我们可以看到,访问者模式主要包含五种角色:

抽象访问者( Visitor):接口或抽象类,该类地冠以了对每一个具体元素( Element)的访问行为 visito方法,其参数就是具体的元素( Element)对象。理论上来说,,Visitor的方法个数与元素( Element)个数是相等的。如果元素( Element)个数经常变动,会导致 Visitor的
方法也要进行变动,此时,该情形并不适用访问者模式

具体访问者( ConcreteVisitor):实现对具体元素的操作

抽象元素( Element):接口或抽象类,定义了一个接受访问者访问的方法 accept(),表示所有元素类型都支持被访问者访问

具体元素( ConcreteElement):具体元素类型,提供接受访问者的具体实现。通常的实现都为: Visitor. visit(this)

结构对象( ObjectStruture):该类内部维护了元素集合,并提供方法接受访问者对该集合所有元素进行操作。

访问者模的应用场景

访问者模式在生活场景中也是非常当多的,例如每年年底的KPI考核,KP考核标准是相对稳定的,但是参与KPI考核的员工可能每年都会发生变化,那么员工就是访问者。我们平时去食堂或者餐厅吃饭,餐厅的菜单和就餐方式是相对稳定的,但是去餐厅就餐的人员是每天都在
发生变化的,因此就餐人员就是访问者。

当系统中存在类型数目稳定(固定)的一类数据结构时,可以通过访问者模式方便地实现对该类型所有数据结构的不同操作,而又不会数据产生任何副作用(脏数据)。简而言之,就是对集合中的不同类型数据(类型数量稳定)进行多种操作,则使用访问者模式。

下面总结一下访问者模式的适用场景

1、数据结构稳定,作用于数据结构的操作经常变化的场景

2、需要数据结构与数据操作分离的场景

3、需要对不同数据类型(元素)进行操作,而不使用分支判断具体类型的场景、

通用代码

抽象访问者( Visitor)

1
2
3
4
5
public interface IVisitor
{
void visit(ConcreteElementA elementA);
void visit(ConcreteElementB elementB);
}

具体访问者( ConcreteVisitor)

ConcreteVisitorA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ConcreteVisitorA implements IVisitor
{

@Override
public void visit(ConcreteElementA elementA)
{
System.out.print("form ConcreteVisitorA:");
elementA.operationA();
}

@Override
public void visit(ConcreteElementB elementB)
{
System.out.print("form ConcreteVisitorA:");
elementB.operationB();
}
}

ConcreteVisitorB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ConcreteVisitorB implements IVisitor
{

@Override
public void visit(ConcreteElementA elementA)
{
System.out.print("form ConcreteVisitorB:");
elementA.operationA();
}

@Override
public void visit(ConcreteElementB elementB)
{
System.out.print("form ConcreteVisitorB:");
elementB.operationB();
}
}

抽象元素( Element)

1
2
3
4
public interface IElement
{
void accept(IVisitor visitor);
}

具体元素( ConcreteElement)

ConcreteElementA

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ConcreteElementA implements IElement
{
@Override
public void accept(IVisitor visitor)
{
visitor.visit(this);
}

public void operationA()
{
System.out.println("ConcreteElementA operationA");
}
}

ConcreteElementB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ConcreteElementB implements IElement
{

@Override
public void accept(IVisitor visitor)
{
visitor.visit(this);
}

public void operationB()
{
System.out.println("ConcreteElementB operationB");
}
}

结构对象( ObjectStruture)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ObjectStructure
{
private IElement elementA;
private IElement elementB;

public ObjectStructure()
{
this.elementA = new ConcreteElementA();
this.elementB = new ConcreteElementB();
}

public void accept(IVisitor visitor)
{
elementA.accept(visitor);
elementB.accept(visitor);
}
}

测试代码

1
2
3
4
5
6
7
8
9
public class Main
{
public static void main(String[] args)
{
ObjectStructure objectStructure = new ObjectStructure();
objectStructure.accept(new ConcreteVisitorA());
objectStructure.accept(new ConcreteVisitorB());
}
}

输出

1
2
3
4
form ConcreteVisitorA:ConcreteElementA operationA
form ConcreteVisitorA:ConcreteElementB operationB
form ConcreteVisitorB:ConcreteElementA operationA
form ConcreteVisitorB:ConcreteElementB operationB

静态分派和动态分派

变量被声明时的类型叫做变量的静态类型( Static Type),有些人又把静态类型叫做明显类型( Apparent Type);而变量所引用的对象的真实类型又叫做变量的实际类型( Actual Type)。

比如

1
2
List 1ist = nu1l;
list = new Arraylist();

声明了一个变量list,它的静态类型(也叫明显类型)是List,而它的实际类型是 ArrayList。根据对象的类型而对方法进行的选择,就是分派( Dispatch)。分派又分为两种,即静态分派和动态分派。

静态分派

静态分派( Static Dispatch)就是按照变量的静态类型进行分派,从而确定方法的执行版本,静态分派在编译时期就可以确定方法的版本。而静态分派最典型的应用就是方法重载,来看下面这段代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Main
{
public static void test(String string)
{
System.out.println(string);
}

public static void test(Integer integer)
{
System.out.println("integer");
}

public static void main(String[] args)
{
String string = "1";
Integer integer = 1;
test(integer);
test(string);
}
}

在静态分派判断的时候,我们根据多个判断依据(即参数类型和个数)判断出了方法的版本,那么这个就是多分派的概念,因为我们有一个以上的考量标准,所以Java是静态多分派的语言。

动态分派

对于动态分派,与静态相反,它不是在编译期确定的方法版本,而是在运行时才能确定。而动态分派最典型的应用就是多态的特性。举个例子,来看下面的这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface Person{
void test();
}
class Man implements Person{
public void test(){
System.out.printin("男人");
}
}
class Woman implements Person{
public void test()
{
System.out. printin("女人");
}
}
public class Main {
public static void main(String[] args)
{
Person man = new Man();
Person woman = new Woman();
man.test();
woman.test();
}
}

这段程序输出结果为依次打印男人和女人,然而这里的 test()方法版本,就无法根据Man和 Woman的静态类型去判断了,他们的静态类型都是 Person接口,根本无从判断。

显然,产生的输出结果,就是因为test()方法的版本是在运行时判断的,这就是动态分派。

动态分派判断的方法是在运行时获取到Man和 Woman的实际引用类型,再确定方法的版本,而由于此时判断的依据只是实际引用类型,只有一个判断依据,所以这就是单分派的概念

这时我们的考量标准只有一个,即变量的实际引用类型。相应的,这说明Java是动态单分派的语言。

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

通过前面分析,我们知道Java是静态多分派、动态单分派的语言。Java底层不支持动态的双分派。但是通过使用设计模式,也可以在 Java语言里实现伪动态双分派。在访问者模式中使用的就是伪动态双分派。所谓动态双分派就是在运行时依据两个实际类型去判断一个方法的运行行为,而访问者模式实现的手段是进行了两次动态单分派来达到这个效果。

例如上述通用代码的例子,就是依据ConcreteVisitorA和 ConcreteVisitorB两个实际类型决定了 accept()方法的执行结果。

分析 accept0方法的调用过程

1、当调用 accept()方法时根据 ConcreteVisitor 的实际类型决定是调用ConcreteVisitorA还是 ConcreteVisitorB 的 accept()方法
2、这时 accept())方法的版本已经确定,假如是 ConcreteVisitorA,它的 accept()方法是调用下面这行代码

1
2
3
4
5
@Override
public void accept(IVisitor visitor)
{
visitor.visit(this);
}

此时的this是 ConcreteVisitorA 类型,所以对应的是 Visitor接口的 visit(ConcreteElementA elementA)方法,此时需要再根据访问者的实际类型确定 Visit()方法的版本,如此一来,就完成了动态双分派的过程。

以上的过程就是通过两次动态双分派,第一次对 accept()方法进行动态分派,第二次对访问者的 visit()方法进行动态分派,从而达到了根据两个实际类型确定一个方法的行为的效果。而原本我们的做法,通常是传入一个接口,直接使用该接口的方法,此为动态单分派,就像策略模式一样。在这里,accept() 方法传入的访问者接口并不是直接调用自己的 visit()方法,而是通过 ConcreteVisitor 的实际类型先动态分派一次,然后在分派后确定的方法版本里再进行自己的动态分派。

访问者模式在源码中的应用

首先来看JDK的NIO模块下的 Filevisitor,它接口提供了递归遍历文件树的支持。这个接口上的方法表示了遍历过程中的关键过程,允许你在文件被访问、目录将被访问、目录已被访问、发生错误等等过程上进行控制;换句话说,这个接口在文件被访问前、访问中和访问后,以及产生错误的时候都有相应的钩子程序进行处理。

访问者模式在 Spring中的应用,Spring loc中有个 BeanDefinitionVisitor类其中有一个 visitBeanDefinition()方法就是使用的访问者模式。

访问者模式的优缺点

优点

1、解耦了数据结构与数据操作,使得操作集合可以独立变化;

2、扩展性好:可以通过扩展访问者角色,实现对数据集的不同操作:

3、元素具体类型并非单一,访问者均可操作;

4、各角色职责分离,符合单一职责原则。

缺点

1、无法增加元素类型:若是统数据结构对象易于变化,经常有新的数据对象增加进来,则访问者类必须增加对应元素类型的操作,违背了开闭原则;

2、具体元素变更困难:具体元素增加属性,删除属性等操作会导致对应的访问者类需要进行相应的修改,尤其当有大量访问者类时,修改范围太大;

3、违背依赖倒置原则:为了达到”区别对待“,访问者依赖的是具体元素类型,而不是抽象。

打赏

请我喝杯咖啡吧~

支付宝
微信