基本需求
- 电脑需要键盘鼠标等固定的组件组成
- 现在分为个人,组织等去买电脑,而同一种组件对不同的人(访问者)做出不同的折扣,从而电脑的价格也不一样
- 传统的解决方法:在组件内部进行判断访问人的类型,从而进行不同打出不同的折扣
- 缺陷:如果访问者的类型增加了,则需要改变组件内部的判断代码,违反了开闭原则,访问者的类型太多,判断的代码也会很庞大
基本介绍
-
在访问者模式(Visitor)中,我们使用了一个访问者类,它改变了元素类的执行算法。通过这种方式,元素的执行算法可以随着访问者改变而改变。这种类型的设计模式属于行为型模式。根据模式,元素对象已接受访问者对象,这样访问者对象就可以处理元素对象上的操作
-
也就是说,被访问者可以根据不同的访问者做出不同的响应
-
封装一些作用于某种数据结构的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作
-
主要将数据结构与数据操作分离,解决 数据结构和操作耦合性问题
-
访问者模式的基本工作原理是:在被访问的类里面加一个对外提供接待访问者的接口
-
访问者模式主要应用场景是:需要对一个对象结构中的对象进行很多不同操作(这些操作彼此没有关联),同时需要避免让这些操作"污染"这些对象的类,可以选用访问者模式解决
-
UML类图(原理)
-
说明
- Visitor是抽象访问者,为该对象结构中的ConcreteElement的每一个类声明一个visit操作
- ConcreteVisitor是一个具体的访问值实现每个有Visitor声明的操作,是每个操作实现的部分
- ObjectStructure能枚举它的元素,可以提供一个高层的接口,用来允许访问者访问元素
- Element定义一个accept 方法,接收一个访问者对象
- ConcreteElement为具体元素,实现了accept方法
- 核心思想就是:不同访问者通过accept访问相同的被访问者,被访问者根据访问者携带的方法做出具体的动作,从而达到相同的被访问者再不同的场景下做出不同的响应,对被访问者没有任何影响,也就是双分派
- 双分派说明
- 双分派是指不管类怎么变化,我们都能找到期望的方法运行。双分派意味着得到执行的操作取决于请求的种类和两个接收者的类型
- 即首先在客户端程序中,accept将具体状态作为参数传递Element中,第一次分派
- 然后Element类调用作为参数的 "具体方法" 中方法operation1, 同时将自己(this)作为参数,完成第二次分派
- 所以,要求了visitor必须要有第二次分派使用的方法(感觉就像踢皮球,我调用你,最终调用的还是我的方法)
- 双分派说明
-
UML类图(案例)
-
说明
- ComputerPart是被访问者的抽象父类,提供接受访问者的方法,Keyboard和Mouse是其具体实现
- Visitor是访问者父类,其中包含了访问Keyboard和Mouse类的访问回调方法,所以这就要求了被访问的子类是固定的,如果不固定,增加或者删除,就要修改Visitor类中方法数量
- Computer充当了ObjectStructure,聚合了被访问者并使用
-
代码实现
-
public abstract class ComputerPart { // 电脑组件抽象父类 也就是被访问者 提供接收访问者方法 protected String name; protected double price; public ComputerPart(String name, double price) { this.name = name; this.price = price; } // 接收访问者,双分派中第一次分派 public abstract double accept(Visitor visitor); } // 子类一 class Keyboard extends ComputerPart{ public Keyboard(String name, double price) { super(name, price); } @Override public double accept(Visitor visitor) { // 调用访问者的访问回调方法,将自身再传递给访问者,第二次分派,对不同的访问者做出不同的响应 // 这个this是关键,也是重点 return visitor.visitKeyboard(this); } } // 子类二 class Mouse extends ComputerPart{ public Mouse(String name, double price) { super(name, price); } @Override public double accept(Visitor visitor) { // 调用访问者的访问回调方法,将自身再传递给访问者,第二次分派,对不同的访问者做出不同的响应 // 这个this是关键,也是重点 return visitor.visitMouse(this); } }
-
public interface Visitor { // 访问者抽象父接口 其中的访问回调方法(参数为具体的被访问者)需要包含所有的访问者实现 // 所以这就要求了被访问的子类是固定的,如果不固定,增加或者删除,就要修改Visitor类中方法数量 // 访问Keyboard的访问回调 这样不同的访问者访问同一个被访问者得到的结果都不同 double visitKeyboard(Keyboard keyboard); // 访问Mouse的访问回调 double visitMouse(Mouse mouse); } // 子类一 个人用户 为方便都是九折 class PersonVisitor implements Visitor { @Override public double visitKeyboard(Keyboard keyboard) { return keyboard.price * 0.9; } @Override public double visitMouse(Mouse mouse) { return mouse.price * 0.9; } } // 子类二 群体用户 为方便都是八折 class GroupVisitor implements Visitor { @Override public double visitKeyboard(Keyboard keyboard) { return keyboard.price * 0.8; } @Override public double visitMouse(Mouse mouse) { return mouse.price * 0.8; } }
-
public class Computer { // 此类作为 ObjectStructure,聚合了被访问者并使用 private Keyboard keyboard; private Mouse mouse; public Computer(Keyboard keyboard, Mouse mouse) { this.keyboard = keyboard; this.mouse = mouse; } // 根据不同的访问者获取电脑的价格 public double getPrice(Visitor visitor) { // 被访问者就收访问者 return keyboard.accept(visitor) + mouse.accept(visitor); } }
-
public class Client { public static void main(String[] args) { // 创建鼠标键盘和电脑 Keyboard keyboard = new Keyboard("keyboard", 100d); Mouse mouse = new Mouse("mouse", 100d); Computer computer = new Computer(keyboard, mouse); // 使用不同的访问者获取电脑的价格 PersonVisitor personVisitor = new PersonVisitor(); System.out.println("个人获得的电脑价格是:" + computer.getPrice(personVisitor)); GroupVisitor groupVisitor = new GroupVisitor(); System.out.println("群体获得的电脑价格是:" + computer.getPrice(groupVisitor)); // 如果还有其他的访问者,直接增加其实现类即可,不用改动其他代码,被访问者就可以做出不同的响应 } }
-
注意事项
- 访问者模式符合单一职责原则,让程序有优秀的扩展性、灵活性非常高
- 访问者模式可以对功能进行统一,可以做报表、ui、拦截器与过滤器,适用于数据结构相对稳定的系统
- 具体元素对访问者公布细节,也就是说访问者关注了其他类的内部细节,这是迪米特法则所不建议的,这样造成了具体元素变更比较困难
- 违反了依赖倒转原则,访问者依赖的是具体元素,而不是抽象元素
- 如果一个系统有比较稳定的数据结构,又有经常变化的功能需求,那么访问者模式是比较适合的