写在前面
- 记录学习设计模式的笔记
- 提高对设计模式的灵活运用
学习地址
https://www.bilibili.com/video/BV1G4411c7N4
https://www.bilibili.com/video/BV1Np4y1z7BU
参考文章
http://c.biancheng.net/view/1317.html
项目源码
https://gitee.com/zhuang-kang/DesignPattern
19,访问者模式
19.1 访问者模式的定义和特点
访问者(Visitor)模式的定义:将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构进行分离,是行为类模式中最复杂的一种模式。
访问者(Visitor)模式是一种对象行为型模式,其主要优点如下。
- 扩展性好。能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
- 复用性好。可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。
- 灵活性好。访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。
- 符合单一职责原则。访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。
访问者(Visitor)模式的主要缺点如下。
- 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
- 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。
- 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。
19.2 访问者模式的结构与实现
19.2.1 访问者模式的结构
- 抽象访问者(Visitor)角色:定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作 visit() ,该操作中的参数类型标识了被访问的具体元素。
- 具体访问者(ConcreteVisitor)角色:实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。
- 抽象元素(Element)角色:声明一个包含接受操作 accept() 的接口,被接受的访问者对象作为 accept() 方法的参数。
- 具体元素(ConcreteElement)角色:实现抽象元素角色提供的 accept() 操作,其方法体通常都是 visitor.visit(this) ,另外具体元素中可能还包含本身业务逻辑的相关操作。
- 对象结构(Object Structure)角色:是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map 等聚合类实现。
19.2.2 代码实现
现在养宠物的人特别多,我们就以这个为例,当然宠物还分为狗,猫等,要给宠物喂食的话,主人可以喂,其他人也可以喂食。
- 访问者角色:给宠物喂食的人
- 具体访问者角色:主人、其他人
- 抽象元素角色:动物抽象类
- 具体元素角色:宠物狗、宠物猫
- 结构对象角色:主人家
Person
package com.zhuang.visitor;
/**
* @Classname Person
* @Description 抽象访问者接口
* @Date 2021/3/27 16:48
* @Created by dell
*/
public interface Person {
//喂宠物狗
void feed(Dog dog);
//喂宠物猫
void feed(Cat cat);
}
Owner
package com.zhuang.visitor;
/**
* @Classname Owner
* @Description 具体访问者角色 主人类
* @Date 2021/3/27 16:49
* @Created by dell
*/
public class Owner implements Person {
@Override
public void feed(Dog dog) {
System.out.println("主人喂食宠物狗...");
}
@Override
public void feed(Cat cat) {
System.out.println("主人喂食宠物猫...");
}
}
Someone
package com.zhuang.visitor;
/**
* @Classname Someone
* @Description 具体访问者角色 其他人类
* @Date 2021/3/27 16:49
* @Created by dell
*/
public class Someone implements Person {
@Override
public void feed(Dog dog) {
System.out.println("其他人喂食宠物狗...");
}
@Override
public void feed(Cat cat) {
System.out.println("其他人喂食宠物猫...");
}
}
Animal
package com.zhuang.visitor;
/**
* @Classname Animal
* @Description 定义抽象节点
* @Date 2021/3/27 16:50
* @Created by dell
*/
public interface Animal {
void accept(Person person);
}
Dog
package com.zhuang.visitor;
/**
* @Classname Dog
* @Description 具体节点 实现Animal接口
* @Date 2021/3/27 16:48
* @Created by dell
*/
public class Dog implements Animal {
@Override
public void accept(Person person) {
person.feed(this);
System.out.println("真香~,汪汪汪!!!");
}
}
Cat
package com.zhuang.visitor;
/**
* @Classname Cat
* @Description 用一句话描述类的作用
* @Date 2021/3/27 16:49
* @Created by dell
*/
public class Cat implements Animal {
@Override
public void accept(Person person) {
person.feed(this);
System.out.println("真香~,喵喵喵!!!");
}
}
Home
package com.zhuang.visitor;
import java.util.ArrayList;
import java.util.List;
/**
* @Classname Home
* @Description 定义对象结构 主人的家
* @Date 2021/3/27 16:50
* @Created by dell
*/
public class Home {
private List<Animal> nodeList = new ArrayList<Animal>();
//添加方法
public void add(Animal animal) {
nodeList.add(animal);
}
public void aciton(Person person) {
for (Animal node : nodeList) {
node.accept(person);
}
}
}
Client
package com.zhuang.visitor;
/**
* @Classname Client
* @Description 访问者模式 测试类
* @Date 2021/3/27 16:50
* @Created by dell
*/
public class Client {
public static void main(String[] args) {
Home home = new Home();
home.add(new Dog());
home.add(new Cat());
Owner owner = new Owner();
home.aciton(owner);
System.out.println("===========================");
Someone someone = new Someone();
home.aciton(someone);
}
}
19.3 扩展
访问者模式用到了一种双分派的技术。
1,分派:
变量被声明时的类型叫做变量的静态类型,有些人又把静态类型叫做明显类型;而变量所引用的对象的真实类型又叫做变量的实际类型。比如 Map map = new HashMap()
,map变量的静态类型是 Map
,实际类型是 HashMap
。根据对象的类型而对方法进行的选择,就是分派(Dispatch),分派(Dispatch)又分为两种,即静态分派和动态分派。
静态分派(Static Dispatch) 发生在编译时期,分派根据静态类型信息发生。静态分派对于我们来说并不陌生,方法重载就是静态分派。
动态分派(Dynamic Dispatch) 发生在运行时期,动态分派动态地置换掉某个方法。Java通过方法的重写支持动态分派。
2,动态分派:
通过方法的重写支持动态分派。
public class Animal {
public void execute() {
System.out.println("Animal");
}
}
public class Dog extends Animal {
@Override
public void execute() {
System.out.println("我是狗...");
}
}
public class Cat extends Animal {
@Override
public void execute() {
System.out.println("我是猫...");
}
}
public class Client {
public static void main(String[] args) {
Animal a = new Dog();
a.execute();
Animal a1 = new Cat();
a1.execute();
}
}
上面代码的结果大家应该直接可以说出来,这不就是多态吗!运行执行的是子类中的方法。
Java编译器在编译时期并不总是知道哪些代码会被执行,因为编译器仅仅知道对象的静态类型,而不知道对象的真实类型;而方法的调用则是根据对象的真实类型,而不是静态类型。
3,静态分派:
通过方法重载支持静态分派。
public class Animal {
}
public class Dog extends Animal {
}
public class Cat extends Animal {
}
public class Execute {
public void execute(Animal a) {
System.out.println("Animal");
}
public void execute(Dog d) {
System.out.println("我是狗...");
}
public void execute(Cat c) {
System.out.println("我是猫...");
}
}
public class Client {
public static void main(String[] args) {
Animal a = new Animal();
Animal a1 = new Dog();
Animal a2 = new Cat();
Execute exe = new Execute();
exe.execute(a);
exe.execute(a1);
exe.execute(a2);
}
}
运行结果:
这个结果可能出乎一些人的意料了,为什么呢?
重载方法的分派是根据静态类型进行的,这个分派过程在编译时期就完成了。
4,双分派:
所谓双分派技术就是在选择一个方法的时候,不仅仅要根据消息接收者(receiver)的运行时区别,还要根据参数的运行时区别。
public class Animal {
public void accept(Execute exe) {
exe.execute(this);
}
}
public class Dog extends Animal {
public void accept(Execute exe) {
exe.execute(this);
}
}
public class Cat extends Animal {
public void accept(Execute exe) {
exe.execute(this);
}
}
public class Execute {
public void execute(Animal a) {
System.out.println("animal");
}
public void execute(Dog d) {
System.out.println("我是狗...");
}
public void execute(Cat c) {
System.out.println("我是猫...");
}
}
public class Client {
public static void main(String[] args) {
Animal a = new Animal();
Animal d = new Dog();
Animal c = new Cat();
Execute exe = new Execute();
a.accept(exe);
d.accept(exe);
c.accept(exe);
}
}
在上面代码中,客户端将Execute对象做为参数传递给Animal类型的变量调用的方法,这里完成第一次分派,这里是方法重写,所以是动态分派,也就是执行实际类型中的方法,同时也将自己this作为参数传递进去,这里就完成了第二次分派
,这里的Execute类中有多个重载的方法,而传递进行的是this,就是具体的实际类型的对象。
说到这里,我们已经明白双分派是怎么回事了,但是它有什么效果呢?就是可以实现方法的动态绑定,我们可以对上面的程序进行修改。
运行结果如下:
双分派实现动态绑定的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动态的,所以重载就是动态的了。
写在最后
- 如果我的文章对你有用,请给我点个👍,感谢你😊!
- 有问题,欢迎在评论区指出!💪