一、什么是访问者模式
访问者模式是一个相对比较简单,但结构又稍显复杂的模式,它讲的是表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。例如,你在朋友家做客,你是访问者,朋友接收你的访问,你通过朋友的描述,然后对朋友的描述做出一个判断,这就是访问者模式。
访问者模式(Visitor),封装一些作用于某种数据结构的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。UML结构图如下:
其中,Visitor是抽象访问者,为该对象结构中ConcreteElement的每一个类声明一个Visit操作;ConcreteVisitor是具体访问者,实现每个由visitor声明的操作,是每个操作实现算法的一部分,而该算法片段是对应于结构中对象的类;ObjectStructure为能枚举它的元素,可以提供一个高层的接口以允许访问者访问它的元素;Element定义了一个Accept操作,它以一个访问者为参数;ConcreteElement为具体元素,实现Accept操作。
1. 抽象访问者
此处可为抽象类或接口,用于声明访问者可以访问哪些元素,具体到程序中就是visit方法的参数定义哪些对象是可以被访问的。
1 public abstract class Visitor { 2 3 public abstract void visitConcreteElementA(ConcreteElementA concreteElementA); 4 5 public abstract void visitConcreteElementB(ConcreteElementB concreteElementB); 6 7 }
2. 具体访问者
影响访问者访问到一个类后该干什么、怎么干。这里以ConcreteVisitor1为例,ConcreteVisitor2就不再赘述了。
1 public class ConcreteVisitor1 extends Visitor { 2 3 @Override 4 public void visitConcreteElementA(ConcreteElementA concreteElementA) { 5 System.out.println(concreteElementA.getClass().getName() + " 被 " + this.getClass().getName() + " 访问"); 6 } 7 8 @Override 9 public void visitConcreteElementB(ConcreteElementB concreteElementB) { 10 System.out.println(concreteElementB.getClass().getName() + " 被 " + this.getClass().getName() + " 访问"); 11 } 12 13 }
3. 抽象元素
此处为接口后抽象类,用于声明接受哪一类访问者访问,程序上是通过accpet方法中的参数来定义的。
抽象元素有两类方法,一是本身的业务逻辑,也就是元素作为一个业务处理单元必须完成的职责;另外一个是允许哪一个访问者来访问。这里只声明的第二类即accept方法。
1 public abstract class Element { 2 3 public abstract void accept(Visitor visitor); 4 5 }
4. 具体元素
实现accept方法,通常是visitor.visit(this)。这里以ConcreteElementA为例,ConcreteElementB就不再赘述了。
1 public class ConcreteElementA extends Element { 2 3 @Override 4 public void accept(Visitor visitor) { 5 visitor.visitConcreteElementA(this); 6 } 7 8 //其它方法 9 public void operationA() { 10 11 } 12 13 }
5. 结构对象
元素生产者,一般容纳在多个不同类、不同接口的容器,如List、Set、Map等,在项目中,一般很少抽象出这个角色。
1 public class ObjectStructure { 2 3 private List<Element> elements = new LinkedList<>(); 4 5 public void attach(Element element) { 6 elements.add(element); 7 } 8 9 public void detach(Element element) { 10 elements.remove(element); 11 } 12 13 public void accept(Visitor visitor) { 14 for (Element element : elements) { 15 element.accept(visitor); 16 } 17 } 18 19 }
6. Client客户端
我们通过以下场景模拟一下访问者模式。
1 public class Client { 2 3 public static void main(String[] args) { 4 ObjectStructure objectStructure = new ObjectStructure(); 5 6 objectStructure.attach(new ConcreteElementA()); 7 objectStructure.attach(new ConcreteElementB()); 8 9 ConcreteVisitor1 visitor1 = new ConcreteVisitor1(); 10 ConcreteVisitor2 visitor2 = new ConcreteVisitor2(); 11 12 objectStructure.accept(visitor1); 13 objectStructure.accept(visitor2); 14 } 15 16 }
运行结果如下:
二、访问者模式的应用
1. 何时使用
- 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作“污染”这些对象的类时
2. 方法
- 在被访问的类里面添加一个对外提供接待访问者的接口
3. 优点
- 符合单一职责原则
- 优秀的扩展性
- 灵活性非常高
4. 缺点
- 具体元素对访问者公布细节,也就是说访问者关注了其他类的内部细节,这是迪米特法则所不建议的
- 具体元素变更比较困难
- 违背了依赖倒转原则。访问者依赖的是具体元素,而不是抽象元素
5. 使用场景
- 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖与其具体类的操作,也就是用迭代器模式已经不能胜任的情景
- 需要对一个对结构中的对象进行很多不同并且不相关的操作,而你想避免让这些操作“污染”这些对象
6. 目的
- 把处理从数据结构分离出来
7. 应用实例
- 人类只分为男人和女人,这个性别分类是稳定的,可以在状态类中,增加“男人反应”和“女人反应”两个方法,方法个数是稳定的,不会很容易发生变化
- 你在朋友家做客,你是访问者,朋友接受你的访问,你通过朋友的描述,然后对朋友的描述做出一个判断
8. 注意事项
- 访问者可以对功能进行统一,可以做报表、UI、拦截器与过滤器
- 访问者模式适用于数据结构相对稳定的系统
三、访问者模式的实现
下面就以上述应用实例中的人类分为男人和女人这个例子来实现访问者模式。UML图如下:
1. Action
抽象的状态类,主要声明以下两个方法。
这里的关键在于人只分男人和女人,这个性别的分类是稳定的,所以可以在状态类中,增加“男人反应”和“女人反应”两个方法,方法个数是稳定的,不会容易发生变化。
1 public abstract class Action { 2 3 //得到男人的结论或反应 4 public abstract void getManConclusion(Man man); 5 6 //得到女人的结论或反应 7 public abstract void getWomanConclusion(Woman woman); 8 9 }
2. Person
人的抽象类。只有一个“接受”的抽象方法,它是用来获得“状态”对象的。
1 public abstract class Person { 2 3 //接受 4 public abstract void accept(Action action); 5 6 }
3. Action类的具体实现类
这里以成功类(Success)为例,失败类(Fail)同理。
1 public class Success extends Action { 2 3 @Override 4 public void getManConclusion(Man man) { 5 System.out.println("男人成功..."); 6 } 7 8 @Override 9 public void getWomanConclusion(Woman woman) { 10 System.out.println("女人成功..."); 11 } 12 13 }
4. Person类的具体实现类
这里以男人类(Man)为例,女人类(Woman)同理。
这里用到了双分派,即首先在客户程序中将具体状态作为参数传递给Man类完成了一次分派,然后Man类调用作为参数的“具体方法”中的方法getManConclusion(),同时将自己(this)作为参数传递进去,这便完成了第二次分派。accept方法就是一个双分派操作,它得到执行的操作不仅决定于Action类的具体状态,还决定于它访问的Person的类别。
1 public class Man extends Person { 2 3 @Override 4 public void accept(Action action) { 5 action.getManConclusion(this); 6 } 7 8 }
5. 结构对象
1 public class ObjectStructure { 2 3 private List<Person> elements = new LinkedList<>(); 4 5 //增加 6 public void attach(Person person) { 7 elements.add(person); 8 } 9 10 //移除 11 public void detach(Person person) { 12 elements.remove(person); 13 } 14 15 //查看显示 16 public void display(Action action) { 17 for (Person person : elements) { 18 person.accept(action); 19 } 20 } 21 22 }
6. Client客户端
1 public class Client { 2 3 public static void main(String[] args) { 4 ObjectStructure objectStructure = new ObjectStructure(); 5 6 objectStructure.attach(new Man()); 7 objectStructure.attach(new Woman()); 8 9 //成功 10 Success success = new Success(); 11 objectStructure.display(success); 12 13 //失败 14 Failing failing = new Failing(); 15 objectStructure.display(failing); 16 } 17 18 }
运行结果如下:
四、双分派
上面提到了双分派,所谓双分派是指不管类怎么变化,我们都能找到期望的方法运行。双分派意味着得到执行的操作取决于请求的种类和两个接收者的类型。
以上述实例为例,假设我们要添加一个Marray的状态类来考察Man类和Woman类的反应,由于使用了双分派,只需增加一个Action子类即可在客户端调用来查看,不需要改动任何其他类的代码。
而单分派语言处理一个操作是根据请求者的名称和接收到的参数决定的,在Java中有静态绑定和动态绑定之说,它的实现是依据重载和重写实现的。值得一提的是,Java是一个支持双分派的单分派语言。