设计模式学习笔记(二十六):访问者模式
1 概述
1.1 引言
患者就医时,医生开具处方后通常由药房工作人员准备药品,由划价人员根据药品数量计算总价,这里,可以将处方看作是一个药品信息的集合,里面包含了一种或多种不同类型的药品信息,不同类型的工作人员在操作同一个药品信息集合时将提供不同的处理方式,而且可能还会增加新类型的工作人员来操作处方单。
在软件开发中,有时候也需要处理像处方单这样的集合结构,在该对象结构中存储了多个不同类型的对象信息,而且对同一对象结构中的元素的操作方式不唯一,可能需要提供多种不同的处理方式,还有可能增加新的处理方式。这时候可以使用访问者模式进行处理。
访问者模式是一种较为复杂的行为型设计模式,它包含访问者与被访问者两个主要组成部分,这些被访问的元素通常具有不同的类型,且不同的访问者可以对它们进行不同的访问操作。访问者模式使得用户可以在不修改现有系统的情况下扩展系统的功能,为这些不同类型的元素增加新的操作。
1.2 定义
访问者模式:提供一个作用于某对象结构中的各元素的操作表示,它使得可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
访问者模式是一种对象行为型模式。
1.3 结构图
1.4 角色
Visitor
(抽象访问者):为每一个具体元素类声明一个具体访问者的操作ConcreteVisitor
(具体访问者):实现抽象访问者中的操作Element
(抽象元素):接口/抽象类,定义一个accept
方法表示接受访问者的访问ConcreteElement
(具体元素):实现了accept
方法,在accept
中调用访问者的访问方法完成对具体元素的访问ObjectStructure
(对象结构):抽象元素的集合,用于存放抽象元素对象,提供了遍历内部元素的方法
2 典型实现
2.1 步骤
- 定义抽象元素:声明一个
accept
方法表示接受访问者访问,由具体元素实现具体访问操作 - 定义具体元素:实现抽象元素中的
accept
方法,同时定义访问属性的方法供访问者调用 - 定义对象结构:使用
List
或Set
等存储抽象元素集合,包含管理集合元素的方法,同时也包含accept
方法,该方法会遍历元素并调用每个元素的accept
方法 - 定义抽象访问者:声明
visit
方法,作为对具体元素的访问方法,一般使用重载实现,也就是一个具体元素对应一个visit
- 定义具体访问者:实现抽象访问者中的访问具体元素方法
2.2 抽象元素
interface Element
{
void accept(Visitor visitor);
}
这里实现为一个接口,包含一个accept
方法,表示接受访问者的访问。
2.3 具体元素
class ConcreteElementA implements Element
{
@Override
public void accept(Visitor visitor)
{
visitor.visit(this);
}
public void show1()
{
System.out.println("用第一种方式访问具体元素A");
}
public void show2()
{
System.out.println("用第二种方式访问具体元素A");
}
}
class ConcreteElementB implements Element
{
@Override
public void accept(Visitor visitor)
{
visitor.visit(this);
}
public void show1()
{
System.out.println("用第一种方式访问具体元素B");
}
public void show2()
{
System.out.println("用第二种方式访问具体元素B");
}
}
这里定义了两个具体元素,重点是其中的accept
方法,通过参数visitor
,将自身(具体元素类)作为参数调用visit
方法,以表示该访问者(visitor
)访问该元素(this
)。这里涉及到了"双分派",简单来说就是运行时确定抽象访问者(visitor
)以及抽象元素(this
)的具体类型,下面会有一小节详细说明分派的概念。
2.4 对象结构
class ObjectStructure
{
private List<Element> list = new ArrayList<>();
public void accept(Visitor visitor)
{
list.forEach(t->t.accept(visitor));
}
public void add(Element element)
{
list.add(element);
}
}
使用一个集合存储所有的抽象元素,同时提供管理方法以注入或删除具体元素,也包含accept
方法,接收一个抽象访问者参数,表示接受该访问者访问这个对象结构里面的所有具体元素。
2.5 抽象访问者
interface Visitor
{
void visit(ConcreteElementA element);
void visit(ConcreteElementB element);
}
这里重载了visit
实现对不同具体元素的访问。注意一个具体元素类对应一个visit
方法。
2.6 具体访问者
class ConcreteVisitorA implements Visitor
{
@Override
public void visit(ConcreteElementA element)
{
element.show1();
}
@Override
public void visit(ConcreteElementB element)
{
element.show1();
}
}
class ConcreteVisitorB implements Visitor
{
@Override
public void visit(ConcreteElementA element)
{
element.show2();
}
@Override
public void visit(ConcreteElementB element)
{
element.show2();
}
}
实现抽象访问者中的访问方法,获取具体元素对象后,通过该元素对象的公有方法获取其中的内部数据,或者直接调用具体元素对象的某些公有方法。
2.7 客户端
public static void main(String[] args)
{
Element elementA = new ConcreteElementA();
Element elementB = new ConcreteElementB();
ObjectStructure elements = new ObjectStructure();
elements.add(elementA);
elements.add(elementB);
Visitor visitor = new ConcreteVisitorA();
elements.accept(visitor);
visitor = new ConcreteVisitorB();
elements.accept(visitor);
}
客户端只需要针对抽象元素以及抽象访问者进行编程,通过对象结构对元素进行统一的管理,添加具体元素到对象结构后,动态注入不同的访问者以不同的方式访问对象结构中的所有元素。
输出如下:
3 层次结构
访问者模式中对象结构存储了不同类型的元素对象,以供不同访问者访问。访问者模式包括两个层次结构:
- 访问者层次结构:提供了抽象访问者以及具体访问者
- 元素层次结构:提供了抽象元素以及具体元素
相同的访问者可以以不同的方式访问不同的元素,相同的元素可以接受不同访问者以不同方式的访问。
在访问者模式中:
- 新增具体访问者方便:继承/实现抽象访问者即可,同时定义访问不同具体元素的不同方法
- 新增具体元素类麻烦:增加新的具体元素类需要进行大幅度的修改,首先需要新增抽象访问者中对新具体元素的访问方法,其次,原有的具体访问者都需要对新方法进行实现,修改量极大
3.1 新增访问者
新增具体访问者很容易,在上面例子的基础上,只需要实现新增一个类实现抽象访问者接口即可:
class ConcreteVisitorC implements Visitor
{
@Override
public void visit(ConcreteElementA element)
{
element.show1();
}
@Override
public void visit(ConcreteElementB element)
{
element.show2();
}
}
对于客户端只需要在对象结构中在accept
中注入新的访问者即可:
public static void main(String[] args)
{
Element elementA = new ConcreteElementA();
Element elementB = new ConcreteElementB();
ObjectStructure elements = new ObjectStructure();
elements.add(elementA);
elements.add(elementB);
Visitor visitor = new ConcreteVisitorC();
elements.accept(visitor);
}
3.2 新增具体元素
新增具体元素会导致大量源码的修改,在上面例子的基础上,首先增加一个实现抽象元素接口的具体元素:
class ConcreteElementC implements Element
{
@Override
public void accept(Visitor visitor)
{
visitor.visit(this);
}
public void show1()
{
System.out.println("用第一种方式访问具体元素C");
}
public void show2()
{
System.out.println("用第二种方式访问具体元素C");
}
}
这时IDE应该会提示visitor.visit(this)
这行报错,因为抽象访问者接口没有针对新的具体元素类型的visit
方法,也就是说此时需要修改抽象访问者,增加访问新的具体元素类型的visit
方法:
interface Visitor
{
void visit(ConcreteElementA element);
void visit(ConcreteElementB element);
void visit(ConcreteElementC element);//新增
}
但是此时IDE又会提示具体访问者有错误,因为这是抽象访问者是一个接口,而所有的具体访问者都实现了该接口,也就是下一步需要修改所有的具体访问者,增加新的接口方法:
class ConcreteVisitorA implements Visitor
{
@Override
public void visit(ConcreteElementA element)
{
element.show1();
}
@Override
public void visit(ConcreteElementB element)
{
element.show1();
}
@Override
public void visit(ConcreteElementC element) //新增
{
element.show1();
}
}
class ConcreteVisitorB implements Visitor
{
@Override
public void visit(ConcreteElementA element)
{
element.show2();
}
@Override
public void visit(ConcreteElementB element)
{
element.show2();
}
@Override
public void visit(ConcreteElementC element) //新增
{
element.show2();
}
}
对于客户端来说无须修改太多代码,同样创建具体元素后添加到对象结构中:
public static void main(String[] args)
{
Element elementA = new ConcreteElementA();
Element elementB = new ConcreteElementB();
Element elementC = new ConcreteElementC();
ObjectStructure elements = new ObjectStructure();
elements.add(elementA);
elements.add(elementB);
elements.add(elementC);
elements.accept(new ConcreteVisitorA());
}
3.3 扩展总结
新增访问者步骤如下:
- 新建一个实现/继承抽象访问者的具体访问者类
- 客户端中将新的具体访问者传入对象结构的访问方法中
新增元素步骤如下:
- 新建一个实现/继承抽象元素类的具体元素类
- 抽象访问者新增访问该具体元素的方法
- 原有的所有具体访问者新增访问该元素的方法
- 客户端中创建新元素对象后添加到对象结构中
总的来说,这和抽象工厂模式有点类似,对OCP(开闭原则)的支持具有倾斜性,新增访问者(产品族)很容易,新增元素(产品等级结构)需要修改大量代码。
4 实例
设计一个员工信息管理子系统,包括正式员工以及临时工,管理人员是人力资源部以及财务部的人员,两个部门的人员进行的操作不同,使用访问者模式进行设计。
设计如下:
- 抽象元素:
Employee
- 具体元素:
FulltimeEmployee
+ParttimeEmployee
- 对象结构:
EmployeeList
- 抽象访问者:
Department
- 具体访问者:
FADepartment
+HRDepartment
首先是抽象元素的代码:
interface Employee
{
void accept(Department department);
}
只有一个accept
表示接受抽象访问者访问的方法。
具体元素:
class FulltimeEmployee implements Employee
{
private String name;
public FulltimeEmployee(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
@Override
public void accept(Department department)
{
department.visit(this);
}
}
class ParttimeEmployee implements Employee
{
private String name;
public ParttimeEmployee(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
@Override
public void accept(Department department)
{
department.visit(this);
}
}
实现其中的accept
方法,在里面调用抽象访问者的visit
方法,将自身作为参数。
对象结构如下:
class EmployeeList
{
private List<Employee> list = new ArrayList<>();
public void add(Employee employee)
{
list.add(employee);
}
public void accept(Department department)
{
list.forEach(t->t.accept(department));
}
}
accept
会遍历元素集合,实现访问者对每一个具体元素的访问。
抽象访问者如下:
interface Department
{
void visit(FulltimeEmployee employee);
void visit(ParttimeEmployee employee);
}
两个参数不同的visit
,分别表示对这两个不同具体元素的访问操作。
具体访问者:
class FADepartment implements Department
{
@Override
public void visit(FulltimeEmployee employee)
{
System.out.println("财务部访问全职员工"+employee.getName());
}
@Override
public void visit(ParttimeEmployee employee)
{
System.out.println("财务部访问兼职员工"+employee.getName());
}
}
class HRDepartment implements Department
{
@Override
public void visit(FulltimeEmployee employee)
{
System.out.println("人力资源部访问全职员工"+employee.getName());
}
@Override
public void visit(ParttimeEmployee employee)
{
System.out.println("人力资源部访问兼职员工"+employee.getName());
}
}
对于不同的具体元素,不同的具体访问者有不同的处理方法,这里简单处理只是进行输出。
测试:
public static void main(String[] args)
{
Employee fulltimeEmployee = new FulltimeEmployee("A");
Employee parttimeEmployee = new ParttimeEmployee("B");
EmployeeList list = new EmployeeList();
list.add(fulltimeEmployee);
list.add(parttimeEmployee);
list.accept(new HRDepartment());
list.accept(new FADepartment());
}
客户端针对抽象元素以及抽象访问者编程,创建具体元素后添加到对象结构中,接着将具体访问者作为参数传入对象结构的访问方法中。
输出:
5 分派
在访问者模式中涉及到了“伪动态双分派”的概念,首先看一下什么是分派。
5.1 定义
变量被声明时的类型叫静态类型,变量所引用的类型叫实际类型。比如:
List<String> list = new ArrayList<>();
中,list
的静态类型为List
,实际类型为ArrayList
。
根据对象的类型对方法进行的选择,就是分派。
分派按照分派的方式可以分为:
- 静态分派
- 动态分派
按照分派基于的宗量,可以分为:
- 单分派
- 多分派
先来看一下静/动态分派。
5.2 静/动态分派
5.2.1 静态分派
静态分派:发生在编译时期,分派根据静态类型信息发生,比如方法重载。
比如下面的例子:
public class Test
{
public static void main(String[] args)
{
test(Integer.valueOf(1));
test("1");
}
public static void test(String s)
{
System.out.println("String");
}
public static void test(Integer i)
{
System.out.println("Integer");
}
}
对于test
方法,会根据静态类型选择方法版本,依据test
方法的参数类型和参数数量可以确定唯一一个重载方法版本。
5.2.2 动态分派
动态分派:发生在运行时期,动态置换掉某个方法,比如面向对象的多态特性。
与静态分派相反,动态分派在运行时确定具体方法,比如:
public class Test
{
public static void main(String[] args)
{
A b = new B();
A c = new C();
b.test();
c.test();
}
}
interface A
{
void test();
}
class B implements A
{
@Override
public void test()
{
System.out.println("B方法");
}
}
class C implements A
{
@Override
public void test()
{
System.out.println("C方法");
}
}
例子的test
方法,无法根据对象的静态类型去判断,因为都是同一接口,而是在运行时判断,这就是动态分派,运行时获取到对象的具体引用类型,再确定具体的方法。
5.3 单/多分派
在了解单/多分派之前,先了解一下宗量。
一个方法所属的对象叫做方法的接收者,方法的接收者与方法的参数统称为方法的宗量。
比如下面的Test
类:
public class Test
{
public void print(String str){}
}
print()
属于Test
对象,所以接收者就是Test
对象,print()
有参数str
,类型为String
。所以print
的宗量有两个:
- 接收者
Test
- 参数
String str
根据分派基于多少种宗量,可以将分派划分为单分派与多分派:
- 单分派根据一个宗量的类型对方法进行选择
- 多分派根据多个宗量的类型对方法进行选择(双分派是多分派的一种形式,根据两个宗量的类型对方法进行选择)
5.4 Java语言特性
Java是静态多分派,动态单分派语言。
理由如下:
- 静态多分派:从上面方法重载的例子可以看到,静态时确定方法,而且选择方法的依据是多个宗量(方法接收者,方法参数,参数数量,参数顺序),因此可以说的静态多分派
- 动态单分派:从上面动态分派的例子可以知道,Java中动态分派仅仅考虑到方法的接收者,也就是只根据一个宗量(方法接收者)去选择方法,所以说是动态单分派
5.5 伪动态双分派
一个方法根据两个宗量的类型来决定执行不同的代码,这就是双分派。Java是动态单分派语言,也就是不支持动态双分派。但是使用访问者模式可以达到一种“动态双分派”的效果。因为这不是真正的动态双分派,所以加上了一个“伪”,这种“伪动态双分派”其实是通过两次“动态单分派”来实现。
访问者模式的双分派中,不仅要根据被访问者的运行时区别,还要根据访问者的运行时区别,在客户端中将具体访问者作为参数传递给被访问者(具体元素):
@Override
public void accept(Department department)
{
department.visit(this);
}
由于department
是抽象访问者,运行时确定具体调用哪一个具体访问者的visit
,这里完成第一次动态单分派。
另外visit
接受抽象元素作为参数,把具体元素(this
)作为参数传递,根据方法接收者宗量
选择相应的visit
方法,在这里完成第二次动态分派。
也就是说,访问者模式是首先根据访问者的动态单分派,再根据具体元素(被访问者)的动态单分派,来达到“动态双分派”的效果,由于这不是真正的动态双分派,而且Java是动态单分派语言,因此这种机制也叫“伪动态双分派”。
6 主要优点
- 新增访问操作方便:使用访问者模式,增加新的访问操作就意味着增加一个新的具体访问者类,实现简单,符合开闭原则
- 集中访问行为:将有关元素对象的访问行为集中到一个访问者对象中,而不是分散在一个个的元素类中,类的职责更加清晰,有利于对象结构中元素对象的复用,相同的对象结构可以供多个不同的访问者访问
7 主要缺点
- 新增元素类困难:访问者模式中每新增一个元素类以为着抽象访问者角色需要增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作,违背了开闭原则
- 破坏封装:访问者模式要求访问者对象访问并调用每一个元素对象的操作,这意味着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法供访问者访问
8 适用场景
- 一个对象结构中包含多个类型的对象,希望对这些对象实施一些依赖其具体类型的操作。在访问者中针对每一种具体类型都提供了一个访问操作,不同类型的对象可以有不同的访问操作
- 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而且需要避免让这些操作“污染”这些对象的类,也不希望在新增操作时修改这些类。访问者模式将相关的访问操作集中起来定义在访问者类中,对象结构可以被多个不同的访问者者类所使用,将对象本身于对象的访问操作分离
- 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作