访问者模式
访问者模式是一种行为设计模式, 它能将算法与其所作用的对象隔离开来。
访问者模封装一些作用于某种数据结构的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。主要将数据结构与数据操作分离,解决数据结构和操作耦合性问题
概括:在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。
核心:在被访问的类里面加一个对外提供接待访问者的接口; 为整个类层次结构添加 “外部” 操作, 而无需修改这些类的已有代码。
模式结构
- 抽象访问者(Visitor)角色:定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作 visit() ,该操作中的参数类型标识了被访问的具体元素。
- 具体访问者(ConcreteVisitor)角色:实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。
- 抽象元素(Element)角色:声明一个包含接受操作 accept() 的接口,被接受的访问者对象作为 accept() 方法的参数。
- 具体元素(ConcreteElement)角色:实现抽象元素角色提供的 accept() 操作,其方法体通常都是 visitor.visit(this) ,另外具体元素中可能还包含本身业务逻辑的相关操作。
- 对象结构(Object Structure)角色:是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map 等聚合类实现。
类图
优缺点
优点
- 访问者模式符合单一职责原则、让程序具有优秀的扩展性、灵活性非常高
- 扩展性好。能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
- 访问者模式可以对功能进行统一,可以做报表、UI、拦截器与过滤器,适用于数据结构相对稳定的系统
- 可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度
缺点
- 在访问者同某个元素进行交互时, 它们可能没有访问元素私有成员变量和方法的必要权限。
- 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。
- 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。
应用场景
- 如果一个系统有比较稳定的数据结构,又有经常变化的功能需求,那么访问者模式就是比较合适的
- 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构
示例代码
public class ConcreteElement extends Element{
String name;
public ConcreteElement(String name){
this.name = name;
}
@Override
void accept(Visitor visitor) {
visitor.visit(this);
}
}
public class ConcreteVisitor implements Visitor{
@Override
public void visit(ConcreteElement element) {
System.out.println("访问者模式的访问者"+element.name);
}
}
public abstract class Element {
abstract void accept(Visitor visitor);
}
public class ObjectStruct {
private final LinkedList<Element> elementLinkedList = new LinkedList<>();
public void displayVisitor(Visitor visitor) {
for(Element p: elementLinkedList) {
p.accept(visitor);
}
}
public void add(Element element){
elementLinkedList.add(element);
}
public void remove(Element element) {
elementLinkedList.remove(element);
}
}
public interface Visitor {
void visit(ConcreteElement element);
}
public class VisitorPattern {
public static void main(String[] args) {
ObjectStruct struct = new ObjectStruct();
struct.add(new ConcreteElement("Tom"));
struct.add(new ConcreteElement("jack"));
Visitor visitor = new ConcreteVisitor();
struct.displayVisitor(visitor);
}
}
访问者模式与双分派
分派的概念:
变量被声明时的类型叫做变量的静态类型(Static Type) 又叫明显类型(Apparent Type)。变量所引用的对象的真实类型又叫做变量的实际类型(Actual Type)。
根据对象的类型而对方法进行的选择,就是分派(Dispatch)。
根据分派发生的时期,可以将分派分为两种,即静态分派和动态分派。
静态分派和动态分派
静态分派(Static Dispatch) 发生在编译时期,分派根据静态类型信息发生。方法重载(Overload)就是静态分派的最典型的应用。(所谓的:编译时多态)
动态分派(Dynamic Dispatch) 发生在运行时期,动态分派动态地置换掉某个方法。面向对象的语言利用动态分派来实现方法置换产生的多态性。(所谓的:运行时多态)
也就是,运行的时候,根据参数的类型,选择合适的重载方法,就是动态分派
双分派是一个允许在重载时使用动态绑定的技巧
class Visitor is
method visit(s: Shape) is
print("访问形状")
method visit(d: Dot)
print("访问点")
interface Graphic is
method accept(v: Visitor)
class Shape implements Graphic is
method accept(v: Visitor)
// 编译器明确知道 `this` 的类型是 `Shape`。
// 因此可以安全地调用 `visit(s: Shape)`。
v.visit(this)
class Dot extends Shape is
method accept(v: Visitor)
// 编译器明确知道 `this` 的类型是 `Dot`。
// 因此可以安全地调用 `visit(s: Dot)`。
v.visit(this)
Visitor v = new Visitor();
Graphic g = new Dot();
// `accept` 方法是重写而不是重载的。编译器可以进行动态绑定。
// 因此在对象调用某个方法时,将执行其所属类中的 `accept`
// 方法(在本例中是 `Dot` 类)。
g.accept(v);
// 输出:"访问点"
运行期间,虚拟机对方法的选择,也就是动态分派的过程,虚拟机不会关心传递过来的参数的类型是哪一个。 参数的静态类型和实际类型都不会对方法的选择产生影响,唯一可以影响虚拟机对方法选择的因素只有此方法的接收者的实际类型 ,因为只有一个选择的依据(就是接收者的实际的类型) 所以,Java语言的动态分派属于单分派
访问者模式的代码中
public void displayVisitor(Visitor visitor) {
for(Element p: elementLinkedList) {
p.accept(visitor);
}
}
p.accept(visitor); 所有的p接收的方法的类型都是visitor类型,方法的参数类型不会影响虚拟机对方法的选择,虚拟机具体是调用ElementA的accept()方法还是调用ElementB的accept()方法,是由p 的实际的类型来决定的,在此完成了一次动态单分派
而 accept(visitor)方法的实现
visitor.visit(this)
在运行时根据this的具体类型来选择是调用visitor的 visit(ElementA element)方法还是调用visit(ElementB element)方法,在此完成了一次动态的单分派
两次动态单分派结合起来,就完成了一次动态双分派