思考并回答以下问题:
本章导学
访问者模式是一种较为复杂的行为型设计模式,它包含访问者和被访问元素两个主要组成部分,这些被访问元素具有不同的类型,且不同的访问者可以对其施加不同的访问操作。访问者模式使得用户可以在不修改现有系统的情况下扩展系统的功能,为这些不同类型的元素增加新的操作。
本章将学习访问者模式的定义与结构,理解访问者模式中对象结构的作用以及学会如何编程实现访问者模式,并掌握元素类和访问者类的设计原理及实现过程。
本章知识点
- 访问者模式的定义
- 访问者模式的结构
- 访问者模式的实现
- 访问者模式的应用
- 访问者模式的优缺点
- 访问者模式的适用环境
- 访问者模式与组合模式联用
访问者模式概述
在医生开具处方单(药单)后,很多医院都存在着这样的处理流程:划价人员拿到处方单之后根据药品名称和数量计算总价,药房工作人员根据药品名称和数量准备药品,如图1所示。
图1 医院处方单处理示意图
在图1中,可以将处方单看成一个药品信息的集合,其中包含了一种或多种不同类型的药品信息,不同类型的工作人员(例如划价人员和药房工作人员)在操作同一个药品信息集合时将提供不同的处理方式,而且可能会增加新类型的工作人员来操作处方单。
在软件开发中,有时也需要处理像处方单这样的集合对象结构,在该对象结构中存储了多种不同类型的对象信息,而且对同一对象结构中的元素的操作方式并不唯一,可能需要提供多种不同的处理方式,还有可能增加新的处理方式。在设计模式中,有一种模式可以满足上述要求,它以不同的方式操作复杂对象结构,该模式就是访问者模式。
访问者模式是一种较为复杂的行为型设计模式,它包含访问者和被访问元素两个主要组成部分,这些被访问的元素通常具有不同的类型,且不同的访问者可以对它们进行不同的访问操作。例如处方单中的各种药品信息就是被访问的元素,而划价人员和药房工作人员就是访问者。访问者模式使得用户可以在不修改现有系统的情况下扩展系统的功能,为这些不同类型的元素增加新的操作。
在使用访问者模式时,被访问元素通常不是单独存在的,它们存储在一个集合中,这个集合被称为“对象结构”,访问者通过遍历对象结构实现对其中存储的元素的逐个操作。访问者模式的定义如下:1
访问者模式:表示一个作用于某对象结构中的各个元素的操作。访问者模式让你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
访问者模式是一种对象行为型模式,它为操作存储不同类型元素的对象结构提供了一种解决方案,用户可以对不同类型的元素施加不同的操作。
访问者模式的结构与实现
访问者模式的结构
访问者模式的结构较为复杂,其结构如图2所示。
图2 访问者模式结构图
由图2可知,访问者模式包含以下5个角色。
(1)Visitor(抽象访问者):抽象访问者为对象结构中的每一个具体元素类声明一个访问操作,从这个操作的名称或参数类型大家可以清楚地知道需要访问的具体元素的类型,具体访问者需要实现这些操作方法,定义对这些元素的访问操作。
(2)ConcreteVisitor(具体访问者):具体访问者实现了每个由抽象访问者声明的操作,每一个操作用于访问对象结构中一种类型的元素。
(3)Element(抽象元素):抽象元素一般是抽象类或者接口,它声明了一个Accept()方法,用于接受访问者的访问操作,该方法通常以一个抽象访问者作为参数。
(4)ConcreteElement(具体元素):具体元素实现了Accept()方法,在Accept()方法中调用访问者的访问方法以便完成对一个元素的操作。
(5)Objectistructure(对象结构):对象结构是一个元素的集合,用于存放元素对象,并且提供了遍历其内部元素的方法。对象结构可以结合组合模式来实现,也可以是一个简单的集合对象。
访问者模式的实现
在访问者模式中,对象结构存储了不同类型的元素对象,以供不同的访问者访问。访问者模式包括两个层次结构:一个是访问者层次结构,提供了抽象访问者和具体访问者;另一个是元素层次结构,提供了抽象元素和具体元素。相同的访问者可以以不同的方式访问不同的元素,相同的元素可以接受不同访问者以不同的方式访问。在访问者模式中,增加新的访问者无须修改原有系统,系统具有较好的可扩展性。
在访问者模式中,抽象访问者定义了访问元素对象的方法,通常为每一种类型的元素对象都提供一个访问方法,而具体访问者可以实现这些访问方法。这些访问方法的命名一般有两种方式:一种是直接在方法名中标明待访问元素对象的具体类型,例如VisitElementA(ElementA elementA);另一种是统一命名为Visit(),通过参数类型的不同来定义一系列重载的Visit()方法。当然,如果所有的访问者对某一类型的元素的访问操作都相同,可以将操作代码移到抽象访问者类中,其典型代码如下:
1 | abstract class Visitor |
在这里使用了重载Visit()方法的方式来定义多个方法,用于操作不同类型的元素对象。在抽象访问者Visitor类的子类ConcreteVisitor中实现了抽象的访问方法,用于定义对不同类型元素对象的操作。具体访问者类的典型代码如下:1
2
3
4
5
6
7
8
9
10
11
12class ConcreteVisitor : Visitor
{
public override void Visit(ConcreteElementA elementA)
{
//元素ConcreteElementA操作代码
}
public override void Visit(ConcreteElementB elementB)
{
//元素ConcreteElementB操作代码
}
}
对于元素类而言,在其中一般都定义了一个Accept()方法,用于接收访问者的访问。典型的抽象元素类代码如下:1
2
3
4interface Element
{
void Accept(Visitor visitor);
}
需要注意的是,该方法传入了一个抽象访问者Visitor类型的参数,即针对抽象访问者进行编程,而不是具体访问者,在程序运行时再确定具体访问者的类型,并调用具体访问者对象的Visit()方法实现对元素对象的操作。在抽象元素类Element的子类中实现了Accept()方法,用于接受访问者的访问,在具体元素类中还可以定义不同类型的元素所特有的业务方法。其典型代码如下:1
2
3
4
5
6
7
8
9
10
11
12class ConcreteElementA : Element
{
public void Accept(Visitor visitor)
{
visitor.Visit(this);
}
public void OperationA()
{
//业务方法
}
}
在具体元素类ConcreteElementA的Accept()方法中,通过调用Visitor类的Visit()方法实现对元素的访问,并以当前对象作为Visit()方法的参数。其具体执行过程如下:
(1)调用具体元素类的Accept(Visitor visitor)方法,并将Visitor子类对象作为其参数。
(2)在具体元素类Accept(Visitor visitor)方法内部调用传入的Visitor对象的Visit()方法,例如Visit(ConcreteElementA elementA),将当前具体元素类对象(this)作为参数,例如visitor.Visit(this)。
(3)执行Visitor对象的Visit()方法,在其中还可以调用具体元素对象的业务方法。
这种调用机制也称为“双重分派”,正因为使用了双重分派机制,使得增加新的访问者无须修改现有类库代码,只需将新的访问者对象作为参数传入具体元素对象的Accept()方法,程序运行时将回调在新增Visitor类中定义的Visit()方法,从而增加新的元素访问方式。
在访问者模式中,对象结构是一个集合,用于存储元素对象并接受访问者的访问。其典型代码如下: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
26using System;
using System.Collections.Generic;
class ObjectStructure
{
private List<Element> list = new List<Element>(); //定义一个集合用于存储元素对象
//接受访问者的访问操作
public void Accept(Visitor visitor)
{
foreach (Object obj in list)
{
((Element)obj).Accept(visitor); //遍历访问集合中的每一个元素
}
}
public void AddElement(Element element)
{
list.Add(element);
}
public void RemoveElement(Element element)
{
list.Remove(element);
}
}
在对象结构中可以使用迭代器对存储在集合中的元素对象进行遍历,并逐个调用元素对象的Accept()方法,实现对元素对象的访问操作。
访问者模式的应用实例
下面通过一个应用实例来进一步学习和理解访问者模式。
1.实例说明
某公司OA系统中包含一个员工信息管理子系统,该公司员工包括正式员工和临时工,每周人力资源部和财务部等部门需要对员工数据进行汇总,汇总数据包括员工工作时间、员工工资等。该公司的基本制度如下:
(1)正式员工每周工作时间为40小时不同级别、不同部门的员工每周基本工资不同;如果超过40小时,超出部分按照100元/小时作为加班费;如果少于40小时所缺时间按照请假处理,请假所扣工资以80元/小时计算,直到基本工资扣除到零为止。除了记录实际工作时间外,人力资源部需记录加班时长或请假时长,作为员工平时表现的一项依据。
(2)临时工每周工作时间不固定,基本工资按小时计算,不同岗位的临时工小时工资不同。人力资源部只需记录实际工作时间。
人力资源部和财务部工作人员可以根据各自的需要对员工数据进行汇总处理,人力资源部负责汇总每周员工工作时间,而财务部负责计算每周员工工资。
现使用访问者模式设计该系统,绘制类图并使用C#语言编程实现。
2.实例类图
通过分析,本实例的结构如图3所示。
图3 员工数据汇总模块结构图
在图3中,FADepartment表示财务部,HRDepartment表示人力资源部,它们充当具体访问者类,其抽象父类Department充当抽象访问者类;EmployeeList充当对象结构,用于存储员工列表;FulltimeEmployee表示正式员工,ParttimeEmployee表示临时工,它们充当具体元素类,其父接口Employee充当抽象元素类。
3.实例代码
(1)Employee:员工类,充当抽象元素类。
1 | namespace VisitorSample |
(2)FulltimeEmployee:全职员工类,充当具体元素类。
1 | namespace VisitorSample |
(3)ParttimeEmployee:兼职员工类,充当具体元素类。
1 | namespace VisitorSample |
(4)Department:部门类,充当抽象访问者类。
1 | namespace VisitorSample |
(5)FADepartment:财务部类,充当具体访问者类。
1 | using System; |
(6)HRDepartment:人力资源部类,充当具体访问者类。
1 | using System; |
(7)EmployeeList:员工列表类,充当对象结构。
1 | using System; |
(8)配置文件App.config:在配置文件中存储了具体访问者类的类名。
1 |
|
(9)Program:客户端测试类。
1 | using System; |
4.结果及分析
编译并运行程序,输出结果如下:1
2
3
4
5正式员工张无忌的实际工资为:3700元。
正式员工杨过的实际工资为:2000元。
正式员工段誉的实际工资为:2240元。
临时工洪七公的实际工资为:1600元。
临时工郭靖的实际工资为:1080元。
如果需要更换具体访问者类,无须修改源代码,只需修改配置文件即可。例如将访问者类由财务部改为人力资源部,只需将存储在配置文件App.config中的具体访问者类FADepartment改为HRDepartment,代码如下:1
2
3
4
5
6
<configuration>
<appSettings>
<add key="visitor" value="VisitorSample.HRDepartment"/>
</appSettings>
</configuration>
重新运行客户端程序,输出结果如下:1
2
3
4
5
6
7正式员工张无忌的实际工作时间为:45小时。
正式员工张无忌的加班时间为:5小时。
正式员工杨过的实际工作时间为:40小时。
正式员工段誉的实际工作时间为:38小时。
正式员工段誉的请假时间为:2小时。
临时工洪七公的实际工作时间为:20小时。
临时工郭靖的实际工作时间为:18小时。
如果要在系统中增加一种新的访问者,无须修改源代码,只需增加一个新的具体访问者类即可,在该具体访问者中封装了新的操作元素对象的方法。从增加新的访问者的角度来看,访问者模式符合开闭原则。
如果要在系统中增加一种新的具体元素,例如增加一种新的员工类型为“退休人员”,由于原有系统并未提供相应的访问接口(在抽象访问者中没有声明任何访问“退休人员”的方法),所以必须对原有系统进行修改,在原有的抽象访问者类和具体访问者类中增加相应的访问方法。从增加新的元素的角度来看,访问者模式违背了开闭原则。
综上所述,访问者模式与抽象工厂模式类似,对开闭原则的支持具有倾斜性,可以很方便地添加新的访问者,但是添加新的元素较为麻烦。
访问者模式与组合模式联用
在访问者模式中,包含一个用于存储元素对象集合的对象结构,通常可以使用迭代器来遍历对象结构,同时具体元素之间可以存在整体与部分关系,有些元素作为容器对象,有些元素作为成员对象,因此可以使用组合模式来组织元素。引入组合模式后的访问者模式结构如图4所示。
图4 访问者模式与组合模式联用示意图
需要注意的是,在图4所示的结构中,由于叶子元素的遍历操作已经在容器元素中完成,因此要防止单独将已增加到容器元素中的叶子元素再次加入到对象结构中,对象结构中只保存容器元素和孤立的叶子元素。
访问者模式的优缺点与适用环境
由于访问者模式的使用条件较为苛刻,本身结构也较为复杂,所以在实际应用中的使用频率不是特别高。当系统中存在一个较为复杂的对象结构,且不同访问者对其所采取的操作也不相同时,可以考虑使用访问者模式进行设计。在XML文档解析、编译器的设计、复杂集合对象的处理等领域访问者模式得到了一定的应用。
访问者模式的优点
访问者模式的主要优点如下:
- (1)访问者模式增加新的访问操作很方便。使用访问者模式,增加新的访问操作就意味着增加一个新的具体访问者类,实现简单,无须修改源代码,符合开闭原则。
- (2)访问者模式将有关元素对象的访问行为集中到一个访问者对象中,而不是分散在一个个的元素类中。类的职责更加清晰,有利于对象结构中元素对象的复用,相同的对象结构可以供多个不同的访问者访问。
- (3)访问者模式让用户能够在不修改现有元素类层次结构的情况下,定义作用于该层次结构的操作。
访问者模式的缺点
访问者模式的主要缺点如下:
- (1)增加新的元素类很困难。在访问者模式中,每增加一个新的元素类都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作,这违背了开闭原则的要求。
- (2)破坏封装。访问者模式要求访问者对象访问并调用每一个元素对象的操作,这意味着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法供访问者访问。
访问者模式的适用环境
在以下情况下可以考虑使用访问者模式:
- (1)一个对象结构包含多个类型的对象,希望对这些对象实施一些依赖其具体类型的操作。在访问者模式中针对每一种具体的类型都提供了一个访问操作,不同类型的对象可以有不同的访问操作
- (2)需要对一个对象结构中的对象进行很多不同的且不相关的操作,并需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。访问者模式使得用户可以将相关的访问操作集中起来定义在访问者类中,对象结构可以被多个不同的访问者类所使用,将对象本身与对象的访问操作分离。
- (3)对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。
本章小结
(1)访问者模式用于表示一个作用于某对象结构中的各个元素的操作。访问者模式让你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。访问者模式是一种对象行为型模式。
(2)访问者模式包含抽象访问者、具体访问者、抽象元素、具体元素和对象结构5个角色。其中,抽象访问者为对象结构中的每一个具体元素类声明一个访问操作;具体访问者实现了每个由抽象访问者声明的操作;抽象元素声明了一个Accept()方法,用于接受访问者的访问操作;具体元素实现了Accept()方法,在Accept()方法中调用访问者的访问方法以便完成对一个元素的操作;对象结构是一个元素的集合,用于存放元素对象,并且提供了遍历其内部元素的方法。
(3)访问者模式的主要优点是增加新的访问操作很方便;将有关元素对象的访问行为集中到一个访问者对象中,而不是分散在一个个的元素类中,类的职责更加清晰;让用户能够在不修改现有元素类层次结构的情况下,定义作用于该层次结构的操作。其主要缺点是增加新的元素类很困难,而且可能破坏系统的封装性。
(4)访问者模式适用的环境:一个对象结构包含多个类型的对象,希望对这些对象实施一些依赖其具体类型的操作;需要对一个对象结构中的对象进行很多不同的且不相关的操作,并需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类;对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。
(5)在访问者模式中,包含一个用于存储元素对象集合的对象结构,通常可以使用迭代器来遍历对象结构,同时具体元素之间可以存在整体与部分关系,有些元素作为容器对象,有些元素作为成员对象,因此可以使用组合模式来组织元素。