1.简介
封装某些作用于某种数据结构中的各元素的操作,可以实现在不改变数据结构的前提下定义作用于这些元素的新操作。
2.结构:
抽象访问者(Visitor)--声明访问者可以访问哪些元素,具体对程序中就是visit方法中的参数定义哪些对象是可以被访问的。也用来代表对象结构需要新添加的功能
具体访问者(ConcreteVisitor)--具体访问者实现对象,实现要真添加到对象结构中的功能
抽象元素类(Element)--声明哪一类访问者访问,程序上是通过accept方法中的参数来定义的,
具体元素类(ConcreteElement)--对象结构中具体的对象,也是被访问的对象,通常会回调访问者的真实功能,同时开放自身的数据供访问者使用
结构对象(ObjectStructure)--通常包含多个被访问的对象,它可以遍历多个被访问的对象,也可以让访问者访问它的元素。可以是一个集合
3.优点:
符合单一职责原则:凡是适用访问者模式的场景,元素类中需要封装在访问者中的操作必定是与元素类本身关系不大且易变的操作,使用访问者模式一方面符合单一职责原则,另一方面,因为封装的操作通常来说是易变的,所以当发生变化时,就可以在不改变元素类本身的前提下,实现对变化部的扩展
扩展性良好:元素类可以通过接受不同访问者实现对不同操作的扩展
4.适用场景:
一个对象中存在着一些与本对象不相干的(或者关系较弱的操作),为了避免这些操作污染对象,可以使用访问者模式把这些操作封装到访问者中去
一组对象中,存在着相似的操作,为了避免大量重复代码,也可以将这些重复的操作封装到访问者中去
5.访问者模式的调用顺序图:
6.示例:
扩展客户管理的功能
考虑这样一个应用:扩展客户管理的功能。
既然是扩展功能,那么肯定是已经存在一定的功能了,先看看已有的功能:公司的客户分成两大类,一类是企业客户,一类是个人客户,现有的功能非常简单,就是能让客户提出服务申请。目前的程序结构如图
随着业务的发展,需要加强客户管理的功能,假设现需增加如下的功能:
1:客户对公司产品的偏好分析,针对企业客户和个人客户有不同的分析策略,主要是根据以往购买的历史、潜在购买意向等进行分析,对于企业客户还要添加上客户所在行业的发展趋势、客户的发展预期等的分析。
2:客户价值分析,针对企业客户和个人客户,有不同的分析方式和策略。主要是根据购买的金额大小、购买的产品和服务的多少、购买的频率等进行分析。
其实除了这些功能,还有很多潜在的功能,只是现在还没有要求实现,比如:针对不同的客户进行需求调查;针对不同的客户进行满意度分析;客户消费预期分析等等。虽然现在没有要求实现,但不排除今后有可能会要求实现。
不用访问者模式的解决方案:
1:实现思路
要实现上面要求的功能,也不是很困难,一个很基本的想法就是:既然不同类型的客户操作是不同的,那么在不同类型的客户里面分别实现这些功能,不就可以了。
由于这些功能的实现依附于很多其它功能的实现,或者是需要很多其它的业务数据,在示例里面不太好完整的体现其功能实现,都是示意一下,因此提前说明一下。
按照上述的想法,这个时候的程序结构如图
有何问题
以很简单的方式,实现了要求的功能,这种实现有没有什么问题呢?仔细分析上面的实现,发现有两个主要的问题:
1:在企业客户和个人客户的类里面,都分别实现了提出服务请求、进行产品偏好分析、进行客户价值分析等功能,也就是说,这些功能的实现代码是混杂在同一个类里面的;而且相同的功能分散到了不同的类中去实现,这会导致整个系统难以理解、难以维护。
2:更为痛苦的是,采用这样的实现方式,如果要给客户扩展新的功能,比如前面提到的针对不同的客户进行需求调查;针对不同的客户进行满意度分析;客户消费预期分析等等。每次扩展,都需要改动企业客户的类和个人客户的类,当然也可以通过为它们扩展子类的方式,但是这样可能会造成过多的对象层次。
那么有没有办法,能够在不改变客户这个对象结构中各元素类的前提下,为这些类定义新的功能?也就是要求不改变企业客户和个人客户类,就能为企业客户和个人客户类定义新的功能?
使用访问者模式解决:
仔细分析上面的示例,对于客户这个对象结构,不想改变类,又要添加新的功能,很明显就需要一种动态的方式,在运行期间把功能动态地添加到对象结构中去。
可以应用访问者模式来解决这个问题。要使用访问者模式来重写示例,首先就要按照访问者模式的结构,分离出两个类层次来,一个是对应于元素的类层次,一个是对应于访问者的类层次。
对于对应于元素的类层次,现在已经有了,就是客户的对象层次。而对应于访问者的类层次,现在还没有,不过,按照访问者模式的结构,应该是先定义一个访问者接口,然后把每种业务实现成为一个单独的访问者对象,也就是说应该使用一个访问者对象来实现对客户的偏好分析,而用另外一个访问者对象来实现对客户的价值分析。
理解访问者模式
认识访问者模式
1:访问者的功能
访问者模式能给一系列对象,透明的添加新功能。从而避免在维护期间,对这一系列对象进行修改,而且还能变相实现复用访问者所具有的功能。
由于是针对一系列对象的操作,这也导致,如果只想给一系列对象中的部分对象添加功能,就会有些麻烦;而且要始终能保证把这一系列对象都要调用到,不管是循环也好,还是递归也好,总之要让每个对象都要被访问到。
2:调用通路
访问者之所以能实现“为一系列对象透明的添加新功能”,注意是透明的,也就是这一系列对象是不知道被添加功能的。
重要的就是依靠通用方法,访问者这边说要去访问,就提供一个访问的方法,如visit方法;而对象那边说,好的,我接受你的访问,提供一个接受访问的方法,如accept方法。这两个方法并不代表任何具体的功能,只是构成一个调用的通路,那么真正的功能实现在哪里呢?又如何调用到呢?
很简单,就在accept方法里面,回调visit的方法,从而回调到访问者的具体实现上,而这个访问者的具体实现的方法才是要添加的新的功能。
3:两次分发技术
访问者模式能够实现在不改变对象结构的情况下,就能给对象结构中的类增加功能,实现这个效果所使用的核心技术就是两次分发的技术。
在访问者模式中,当客户端调用ObjectStructure的时候,会遍历ObjectStructure中所有的元素,调用这些元素的accept方法,让这些元素来接受访问,这是请求的第一次分发;在具体的元素对象中实现accept方法的时候,会回调访问者的visit方法,等于请求被第二次分发了,请求被分发给访问者来进行处理,真正实现功能的正是访问者的visit方法。
两次分发技术具体的调用过程示意如图 :
两次分发技术使得客户端的请求不再被静态的绑定在元素对象上,这个时候真正执行什么样的功能同时取决于访问者类型和元素类型,就算是同一种元素类型,只要访问者类型不一样,最终执行的功能也不会一样,这样一来,就可以在元素对象不变的情况下,通过改变访问者的类型,来改变真正执行的功能。
两次分发技术还有一个优点,就是可以在程序运行期间进行动态的功能组装和切换,只需要在客户端调用时,组合使用不同的访问者对象实例即可。
从另一个层面思考,Java回调技术也有点类似于两次分发技术,客户端调用某方法,这个方法就类似于accept方法,传入一个接口的实现对象,这个接口的实现对象就有点像是访问者,在方法内部,会回调这个接口的方法,就类似于调用访问者的visit方法,最终执行的还是接口的具体实现里面实现的功能。
访问模式示例代码:
抽象元素角色:Element
public abstract class Element {
public abstract void accept(IVisitor visitor);
public abstract void doSomething();
}
具体元素角色:ConcreteElement
元素One:
public class ConcreteElementOne extends Element {
@Override
public void accept(IVisitor visitor) {
visitor.visit(this);
}
@Override
public void doSomething() {
System.out.println("这是元素1");
}
}
元素Two:
public class ConcreteElementTwo extends Element {
@Override
public void accept(IVisitor visitor) {
visitor.visit(this);
}
@Override
public void doSomething() {
System.err.println("这是元素2");
}
}
抽象访问者角色:IVisitor
public interface IVisitor {
public void visit(ConcreteElementOne cOne);
public void visit(ConcreteElementTwo cTwo);
}
具体访问者角色:Visitor
public class Visitor implements IVisitor {
@Override
public void visit(ConcreteElementOne cOne) {
cOne.doSomething();
}
@Override
public void visit(ConcreteElementTwo cTwo) {
cTwo.doSomething();
}
}
结构对象:
public class ObjectStructure {
public static List<Element> getList(){
List<Element> list=new ArrayList<Element>();
Random random=new Random();
for(int i=0;i<10;i++){
int a=random.nextInt(100);
if(a>50){
list.add(new ConcreteElementOne());
}else {
list.add(new ConcreteElementTwo());
}
}
return list;
}
}
客户端访问:
public class Client {
public static void main(String[] args) {
List<Element> list=ObjectStructure.getList();
for(Element element:list){
element.accept(new Visitor());
}
}
}