文章目录
- 1 访问者模式(Visitor Pattern)
- 1.1 介绍
- 1.2 概述
- 1.3 访问者模式的结构
- 1.4 访问者模式的优缺点
- 1.5 访问者模式的使用场景
- 2 案例一
- 2.1 需求
- 2.2 代码实现
- 3 案例二
- 3.1 需求
- 3.2 代码实现
- 4 拓展——双分派
- 4.1 分派
- 4.2 动态分派(多态)
- 4.3 静态分派(重载)
- 4.4 双分派
🙊 前言:本文章为瑞_系列专栏之《23种设计模式》的访问者模式篇。本文中的部分图和概念等资料,来源于博主学习设计模式的相关网站《菜鸟教程 | 设计模式》和《黑马程序员Java设计模式详解》,特此注明。本文中涉及到的软件设计模式的概念、背景、优点、分类、以及UML图的基本知识和设计模式的6大法则等知识,建议阅读 《瑞_23种设计模式_概述》
本系列 - 设计模式 - 链接:《瑞_23种设计模式_概述》
⬇️本系列 - 创建型模式 - 链接🔗单例模式:《瑞_23种设计模式_单例模式》
工厂模式:《瑞_23种设计模式_工厂模式》
原型模式:《瑞_23种设计模式_原型模式》
抽象工厂模式:《瑞_23种设计模式_抽象工厂模式》
建造者模式:《瑞_23种设计模式_建造者模式》⬇️本系列 - 结构型模式 - 链接🔗
代理模式:《瑞_23种设计模式_代理模式》
适配器模式:《瑞_23种设计模式_适配器模式》
装饰者模式:《瑞_23种设计模式_装饰者模式》
桥接模式:《瑞_23种设计模式_桥接模式》
外观模式:《瑞_23种设计模式_外观模式》
组合模式:《瑞_23种设计模式_组合模式》
享元模式:《瑞_23种设计模式_享元模式》⬇️本系列 - 行为型模式 - 链接🔗
模板方法模式:《瑞_23种设计模式_模板方法模式》
策略模式:《瑞_23种设计模式_策略模式》
命令模式:《瑞_23种设计模式_命令模式》
职责链模式:《瑞_23种设计模式_职责链模式》
状态模式:《瑞_23种设计模式_状态模式》
观察者模式:《瑞_23种设计模式_观察者模式》
中介者模式:《瑞_23种设计模式_中介者模式》
迭代器模式:《瑞_23种设计模式_迭代器模式》
访问者模式:《后续更新》
备忘录模式:《后续更新》
解释器模式:《后续更新》
1 访问者模式(Visitor Pattern)
在访问者模式(Visitor Pattern)中,我们使用了一个访问者类,它改变了元素类的执行算法。通过这种方式,元素的执行算法可以随着访问者改变而改变。这种类型的设计模式属于行为型模式。根据模式,元素对象已接受访问者对象,这样访问者对象就可以处理元素对象上的操作。
瑞:行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。
瑞:行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。
访问者模式属于:对象行为模式
1.1 介绍
-
意图:主要将数据结构与数据操作分离。
-
主要解决:稳定的数据结构和易变的操作耦合问题。
-
何时使用:需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,使用访问者模式将这些封装到类中。
-
如何解决:在被访问的类里面加一个对外提供接待访问者的接口。
-
关键代码:在数据基础类里面有一个方法接受访问者,将自身引用传入访问者。
-
应用实例:
您在朋友家做客,您是访问者,朋友接受您的访问,您通过朋友的描述,然后对朋友的描述做出一个判断,这就是访问者模式。 -
优点:
1️⃣ 符合单一职责原则。
2️⃣ 优秀的扩展性。
3️⃣ 灵活性。 -
缺点:
1️⃣ 具体元素对访问者公布细节,违反了迪米特原则。
2️⃣ 具体元素变更比较困难。
3️⃣ 违反了依赖倒置原则,依赖了具体类,没有依赖抽象。 -
使用场景:
1️⃣ 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。
2️⃣ 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,也不希望在增加新操作时修改这些类。 -
注意事项:
1️⃣ 访问者可以对功能进行统一,可以做报表、UI、拦截器与过滤器。
1.2 概述
定义:封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作。
访问者模式的主要思想是将一些作用于某种数据结构中的各元素的操作封装起来,使得可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
在访问者模式中,针对系统中拥有固定类型数的对象结构(元素),在其内提供一个accept()方法来接受访问者对象的访问。不同的访问者对同一个元素的访问内容是不同的,这使得相同的元素集合可以产生不同的数据结果。
1.3 访问者模式的结构
- 访问者模式主要包含以下角色:
1️⃣ 抽象访问者(Visitor)角色:定义了对每一个元素(Element)
访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲与元素类个数(Element的实现类个数)是一样的,从这点不难看出,访问者模式要求元素类的个数不能改变。
2️⃣ 具体访问者(ConcreteVisitor)角色:给出对每一个元素类访问时所产生的具体行为。
3️⃣ 抽象元素(Element)角色:定义了一个接受访问者的方法(accept
),其意义是指,每一个元素都要可以被访问者访问。
4️⃣ 具体元素(ConcreteElement)角色: 提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
5️⃣ 对象结构(Object Structure)角色:定义当中所提到的对象结构,对象结构是一个抽象表述,具体点可以理解为一个具有容器性质或者复合对象特性的类,它会含有一组元素(Element
),并且可以迭代这些元素,供访问者访问。
1.4 访问者模式的优缺点
优点:
-
扩展性好
在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。 -
复用性好
通过访问者来定义整个对象结构通用的功能,从而提高复用程度。 -
分离无关行为
通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。
缺点:
-
对象结构变化很困难
在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。 -
违反了依赖倒置原则
访问者模式依赖了具体类,而没有依赖抽象类。
1.5 访问者模式的使用场景
- 对象结构相对稳定,但其操作算法经常变化的程序。
- 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。
2 案例一
【案例】给宠物喂食
2.1 需求
现在养宠物的人特别多,我们就以这个为例,当然宠物还分为狗,猫等,要给宠物喂食的话,主人可以喂,其他人也可以喂食。
- 访问者角色:给宠物喂食的人
- 具体访问者角色:主人、其他人
- 抽象元素角色:动物抽象类
- 具体元素角色:宠物狗、宠物猫
- 结构对象角色:主人家
类图如下:
2.2 代码实现
创建抽象访问者接口
/*** 抽象访问者角色类** @author LiaoYuXing-Ray**/
public interface Person {// 喂食宠物狗void feed(Cat cat);// 喂食宠物猫void feed(Dog dog);
}
创建不同的具体访问者角色(主人和其他人),都需要实现 Person
接口
/*** 具体访问者角色类(自己)** @author LiaoYuXing-Ray**/
public class Owner implements Person {public void feed(Cat cat) {System.out.println("主人喂食猫");}public void feed(Dog dog) {System.out.println("主人喂食狗");}
}
/*** 具体访问者角色类(其他人)** @author LiaoYuXing-Ray**/
public class Someone implements Person {public void feed(Cat cat) {System.out.println("其他人喂食猫");}public void feed(Dog dog) {System.out.println("其他人喂食狗");}
}
定义抽象节点 – 宠物
/*** 抽象元素角色类** @author LiaoYuXing-Ray**/
public interface Animal {// 接受访问者访问的功能void accept(Person person);
}
定义实现Animal
接口的 具体节点(元素)
/*** 具体元素角色类(宠物猫)** @author LiaoYuXing-Ray**/
public class Cat implements Animal {public void accept(Person person) {person.feed(this);// 访问者给宠物猫喂食System.out.println("好好吃,喵喵喵。。。");}
}
/*** 具体元素角色类(宠物狗)** @author LiaoYuXing-Ray**/
public class Dog implements Animal {public void accept(Person person) {person.feed(this);// 访问者给宠物猫喂食System.out.println("好好吃,汪汪汪。。。");}
}
定义对象结构,此案例中就是主人的家
import java.util.ArrayList;
import java.util.List;/*** 对象结构类** @author LiaoYuXing-Ray**/
public class Home {// 声明一个集合对象,用来存储元素对象private final List<Animal> nodeList = new ArrayList<Animal>();// 添加元素功能public void add(Animal animal) {nodeList.add(animal);}public void action(Person person) {// 遍历集合,获取每一个元素,让访问者访问每一个元素for (Animal animal : nodeList) {animal.accept(person);}}
}
/*** 测试类** @author LiaoYuXing-Ray**/
public class Client {public static void main(String[] args) {// 创建Home对象Home home = new Home();//添加元素到Home对象中home.add(new Dog());home.add(new Cat());// 创建主人对象Owner owner = new Owner();Someone someone = new Someone();// 让主人喂食所有的宠物home.action(owner);home.action(someone);}
}
代码运行结果如下:
主人喂食狗好好吃,汪汪汪。。。主人喂食猫好好吃,喵喵喵。。。其他人喂食狗好好吃,汪汪汪。。。其他人喂食猫好好吃,喵喵喵。。。
3 案例二
本案例为菜鸟教程中的案例
3.1 需求
我们将创建一个定义接受操作的 ComputerPart 接口。Keyboard、Mouse、Monitor 和 Computer 是实现了 ComputerPart 接口的实体类。我们将定义另一个接口 ComputerPartVisitor,它定义了访问者类的操作。Computer 使用实体访问者来执行相应的动作。
VisitorPatternDemo,我们的演示类使用 Computer、ComputerPartVisitor 类来演示访问者模式的用法。
3.2 代码实现
步骤 1
定义一个表示元素的接口。
public interface ComputerPart {public void accept(ComputerPartVisitor computerPartVisitor);
}
步骤 2
创建扩展了上述类的实体类。
public class Keyboard implements ComputerPart {@Overridepublic void accept(ComputerPartVisitor computerPartVisitor) {computerPartVisitor.visit(this);}
}
public class Monitor implements ComputerPart {@Overridepublic void accept(ComputerPartVisitor computerPartVisitor) {computerPartVisitor.visit(this);}
}
public class Mouse implements ComputerPart {@Overridepublic void accept(ComputerPartVisitor computerPartVisitor) {computerPartVisitor.visit(this);}
}
public class Computer implements ComputerPart {ComputerPart[] parts;public Computer(){parts = new ComputerPart[] {new Mouse(), new Keyboard(), new Monitor()}; } @Overridepublic void accept(ComputerPartVisitor computerPartVisitor) {for (int i = 0; i < parts.length; i++) {parts[i].accept(computerPartVisitor);}computerPartVisitor.visit(this);}
}
步骤 3
定义一个表示访问者的接口。
public interface ComputerPartVisitor {public void visit(Computer computer);public void visit(Mouse mouse);public void visit(Keyboard keyboard);public void visit(Monitor monitor);
}
步骤 4
创建实现了上述类的实体访问者。
public class ComputerPartDisplayVisitor implements ComputerPartVisitor {@Overridepublic void visit(Computer computer) {System.out.println("Displaying Computer.");}@Overridepublic void visit(Mouse mouse) {System.out.println("Displaying Mouse.");}@Overridepublic void visit(Keyboard keyboard) {System.out.println("Displaying Keyboard.");}@Overridepublic void visit(Monitor monitor) {System.out.println("Displaying Monitor.");}
}
步骤 5
使用 ComputerPartDisplayVisitor 来显示 Computer 的组成部分。
public class VisitorPatternDemo {public static void main(String[] args) {ComputerPart computer = new Computer();computer.accept(new ComputerPartDisplayVisitor());}
}
步骤 6
执行程序,输出结果:
Displaying Mouse.Displaying Keyboard.Displaying Monitor.Displaying Computer.
4 拓展——双分派
访问者模式用到了一种双分派的技术。
4.1 分派
变量被声明时的类型叫做变量的静态类型,有些人又把静态类型叫做明显类型;而变量所引用的对象的真实类型又叫做变量的实际类型。比如 Map map = new HashMap()
,map变量的静态类型是 Map
,实际类型是 HashMap
。根据对象的类型而对方法进行的选择,就是分派(Dispatch),分派(Dispatch)又分为两种,即静态分派和动态分派。
-
静态分派(Static Dispatch) :发生在编译时期,分派根据静态类型信息发生。静态分派对于我们来说并不陌生,方法重载就是静态分派。
-
动态分派(Dynamic Dispatch): 发生在运行时期,动态分派动态地置换掉某个方法。Java通过方法的重写(多态)支持动态分派。
4.2 动态分派(多态)
通过方法的重写(多态)支持动态分派。
public class Animal {public void execute() {System.out.println("Animal");}
}public class Dog extends Animal {@Overridepublic void execute() {System.out.println("dog");}
}public class Cat extends Animal {@Overridepublic void execute() {System.out.println("cat");}
}public class Client {public static void main(String[] args) {Animal a = new Dog();a.execute();Animal a1 = new Cat();a1.execute();}
}
执行程序,输出结果:
dogcat
上面代码的结果大家应该直接可以说出来,这不就是多态吗!运行执行的是子类中的方法。
Java编译器在编译时期并不总是知道哪些代码会被执行,因为编译器仅仅知道对象的静态类型,而不知道对象的真实类型;而方法的调用则是根据对象的真实类型,而不是静态类型。
4.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("dog");}public void execute(Cat c) {System.out.println("cat");}
}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);}
}
执行程序,输出结果:
AnimalAnimalAnimal
这个结果可能出乎一些你的意料了吧~
因为重载方法的分派是根据静态类型进行的,这个分派过程在编译时期就完成了。
4.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("dog");}public void execute(Cat c) {System.out.println("cat");}
}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,就是具体的实际类型的对象。
说到这里,我们已经明白双分派是怎么回事了,但是它有什么效果呢?就是可以实现方法的动态绑定,我们可以对上面的程序进行修改。运行结果如下:
执行程序,输出结果:
animaldogcat
双分派实现动态绑定的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动态的,所以重载就是动态的了
如果觉得这篇文章对您有所帮助的话,请动动小手点波关注💗,你的点赞👍收藏⭐️转发🔗评论📝都是对博主最好的支持~