Visitor design pattern
访问者模式的概念、访问者模式的结构、访问者模式的优缺点、访问者模式的使用场景、访问者模式实现示例、访问者模式的源码分析、双分派
1、访问者模式的概念
访问者模式,即在不改变聚合对象内元素的前提下,为聚合对象内每个元素提供多种访问方式,即聚合对象内的每个元素都有多个访问者对象。访问者模式主要解决稳定的数据结构和易变元素的操作之间的耦合问题。
2、访问者模式的结构
- 抽象元素:定义接受访问者的行为。
- 具体元素:实现抽象访问者,实现其定义的接受访问者的行为,并将访问行为委托给接受的访问者对象。
- 抽象访问者:定义访问不同元素的行为。即当系统中有多个不同类型的元素时,抽象访问者则需要定义多个访问行为。
- 具体访问者:实现抽象访问者,实现其定义的多个访问不同元素的行为。
- 对象结构:即聚合对象,持有一个抽象元素的聚合引用,并提供添加元素、获取元素、移除元素、访问元素的方法。
3、访问者模式的优缺点
- 优点:
- 符合单一职责原则,即数据的存储和操作分别由对象结构类和访问者类实现。
- 优秀的扩展性和灵活性。
- 缺点:
- 具体元素对访问者公布了其细节,违反了迪米特法则。
- 具体元素的增加将导致访问者类的修改,违反了开闭原则。
- 访问者类依赖了具体类而不是抽象,违反了依赖倒置原则。
4、访问者模式的使用场景
- 当对象结构相对稳定,但其操作算法经常变化时。
- 当需要为对象结构中的元素提供多个不同且不想管的操作,且要避免让这些操作的变化影响对象的结构时。
5、访问者模式的实现示例
抽象元素:
public interface Element {
/**
* 接受访问者
* @param visitor
*/
void accept(Visitor visitor);
}
具体元素一:
public class OneElement implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visitor(this);
}
}
具体元素二:
public class TwoElement implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visitor(this);
}
}
抽象访问者:
public interface Visitor {
/**
* 访问元素一
* @param oneElement
*/
void visitor(OneElement oneElement);
/**
* 访问元素二
* @param twoElement
*/
void visitor(TwoElement twoElement);
}
具体访问者一:
public class OneVisitor implements Visitor {
@Override
public void visitor(OneElement oneElement) {
System.out.println("访问者一访问元素一 " + oneElement);
}
@Override
public void visitor(TwoElement twoElement) {
System.out.println("访问者一访问元素二 " + twoElement);
}
}
具体访问者二:
public class TwoVisitor implements Visitor {
@Override
public void visitor(OneElement oneElement) {
System.out.println("访问者二访问元素一 " + oneElement);
}
@Override
public void visitor(TwoElement twoElement) {
System.out.println("访问者二访问元素二 " + twoElement);
}
}
对象结构:
public class ObjectStructure {
private List<Element> elements;
public ObjectStructure() {
this.elements = new ArrayList<>();
}
public void add(Element element) {
this.elements.add(element);
}
public Element get(Integer index) {
return this.elements.get(index);
}
public void remove(Element element) {
this.elements.remove(element);
}
/**
* 访问对象内元素
* @param visitor
*/
public void accept(Visitor visitor) {
for (Element element : this.elements) {
element.accept(visitor);
}
}
}
测试:
public class VisitorTest {
public static void main(String[] args) {
ObjectStructure objectStructure = new ObjectStructure();
objectStructure.add(new OneElement());
objectStructure.add(new OneElement());
objectStructure.add(new TwoElement());
objectStructure.add(new TwoElement());
objectStructure.accept(new OneVisitor());
System.out.println();
objectStructure.accept(new TwoVisitor());
}
}
测试结果:
访问者一访问元素一 org.xgllhz.designpattern.behaviortype.visitor.OneElement@51efea79
访问者一访问元素一 org.xgllhz.designpattern.behaviortype.visitor.OneElement@5034c75a
访问者一访问元素二 org.xgllhz.designpattern.behaviortype.visitor.TwoElement@396a51ab
访问者一访问元素二 org.xgllhz.designpattern.behaviortype.visitor.TwoElement@51081592
访问者二访问元素一 org.xgllhz.designpattern.behaviortype.visitor.OneElement@51efea79
访问者二访问元素一 org.xgllhz.designpattern.behaviortype.visitor.OneElement@5034c75a
访问者二访问元素二 org.xgllhz.designpattern.behaviortype.visitor.TwoElement@396a51ab
访问者二访问元素二 org.xgllhz.designpattern.behaviortype.visitor.TwoElement@51081592
6、访问者模式的源码分析
404
7、双分派技术
访问者模式的实现使用了一种双分派技术。
7.1、分派
变量被声明时的类型叫做变量的静态类型,又称为明显类型;而变量所引用的对象的真实类型叫做变量的实际类型。如 Map<String, Object> map = new HashMap<>(),map 变量的静态类型是 Map,实际类型是 HashMap。根据对象的类型对方法进行选择,就是分派,即 Dispatch,分派又分为两种,即静态分派和动态分派。
-
静态分派:
静态分派发生在编译期,分派由变量的静态类型决定。方法重载就是静态分派。
-
动态分派:
动态分派发生在运行期,分派由变量的实际类型决定。方法重写就是动态分派。
7.2、静态分派
通过方法重载支持静态分派。
public class Hero {
}
public class Zed extends Hero {
}
public class Fizz extends Hero {
}
public class Execute {
public void execute(Hero hero) {
System.out.println("hero");
}
public void execute(Zed zed) {
System.out.println("zed");
}
public void execute(Fizz fizz) {
System.out.println("fizz");
}
}
测试:
public class StaticDispatchTest {
public static void main(String[] args) {
Execute execute = new Execute();
Hero hero = new Hero();
Hero zed = new Zed();
Hero fizz = new Fizz();
execute.execute(hero);
execute.execute(zed);
execute.execute(fizz);
}
}
测试结果:
hero
hero
hero
静态分派是根据变量的静态类型选择的,且是在编译期选择的,于是变量在编译期就选择了 Hero 类型,即变量的静态类型。
7.3、动态分派
通过方法重写来支持动态分派。
public class Hero {
public void execute() {
System.out.println("hero");
}
}
public class Zed extends Hero {
@Override
public void execute() {
System.out.println("zed");
}
}
public class Fizz extends Hero {
@Override
public void execute() {
System.out.println("fizz");
}
}
测试:
public class DynamicDispatchTest {
public static void main(String[] args) {
Hero hero = new Hero();
Hero zed = new Zed();
Hero fizz = new Fizz();
hero.execute();
zed.execute();
fizz.execute();
}
}
测试结果:
hero
zed
fizz
动态分派是在运行期根据变脸的实际类型决定的,即三个变量的实际类型分别是 Hero、Zed、Fizz。
7.4、双分派
双分派,即选择方法时,不仅要根据方法调用者(即消息接受者)的实际类型来决定,还要根据方法入参的实际类型来决定。
public class Hero {
public void accept(Execute execute) {
execute.execute(this);
}
}
public class Zed extends Hero {
@Override
public void accept(Execute execute) {
execute.execute(this);
}
}
public class Fizz extends Hero {
@Override
public void accept(Execute execute) {
execute.execute(this);
}
}
public class Execute {
public void execute(Hero hero) {
System.out.println("hero");
}
public void execute(Zed zed) {
System.out.println("zed");
}
public void execute(Fizz fizz) {
System.out.println("fizz");
}
}
测试:
public class DoubleDispatchTest {
public static void main(String[] args) {
Execute execute = new Execute();
Hero hero = new Hero();
Hero zed = new Zed();
Hero fizz = new Fizz();
hero.accept(execute);
zed.accept(execute);
fizz.accept(execute);
}
}
测试结果:
hero
zed
fizz
在上述示例中,变量调用 accept 方法,是第一次分派,因为使用了方法重写,所以是动态分派。在 accept 方法的具体执行中调用了 execute 对象的 execute 方法,这是第二次分派,因为 execute 方法进行了重载, 但是在 execute 方法的具体入参中使用了 this,即入参是当前对象,也就是变量的实际类型,所以这里的重载也就变成了动态分派了。
双分派实现动态绑定的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动态的,所以重载也就是动态的了。