1.定义
访问者模式,属于行为型模式的一种,目的是将数据结构和数据操作分离。具体原理就是使用一个访问者类,它改变了元素类的执行算法,通过这种方式,元素的执行算法可以随着访问者的改变而改变。其实就是将对象结构中的不变部分和变化分离,将不变的部分固定起来,然后将变化的部分通过某种方式开放出去,由具体的访问者来定义这部分变化的操作。
这个过程有点像回调,回调的本质就是将需要执行的具体流程通过定义一系列的回调接口,然后向调用者开放回调接口,当调用者需要执行具体操作时,通过回调接口调用我们自定义实现的方法。
从用途上看,访问者模式和模板设计模式也有点类似,都是将不变的部分固定流程封装起来,然后将变化的部分交给其他部分去实现。但是两者实际并不同,一个是结构型一个是行为型,一个通过继承去承载,另一个是通过简单的组合包含去承载,也就决定了模板设计模式强调的是结构上的分离,而访问者模式是对具体行为的分离,个人认为访问者模式看起来更灵活一些,
举个栗子:
以操作系统扫描硬件设备为例,操作系统启动时都需要加载所有的设备,以便统一进行管理,但是不同的操作系统,不同的设备之间都有差异,如果具体反映到操作系统管理层面,就是他们的驱动不同,众多的设备意味着各种的驱动,而且驱动一直在更新,一直在变化,操作系统不可能去主动实现每一种设备驱动。
由于这部分是变化的,那操作系统就可以定义统一的加载驱动的规范,并将其开放给所有的硬件设备厂商,并要求所有的厂商,如果生产的设备想要被该操作系统识别并加载,那请按照这个标准去实现各自的驱动,如此,当设备插入主板,如果存在该设备的驱动,就会被操作系统扫描到,然后加载。操作系统不需要知道具体驱动实现细节,这部分是交给驱动厂商去实现,操作系统只是在合适的时机去调用就可以了。
在这个过程中,操作系统就是被访问者,而具体的设备或者设备驱动就是访问者,设备带着已经实现好的驱动去找操作系统,操作系统拿着驱动程序去驱动设备。
由此引出访问者中的具体角色:
- 抽象访问者角色(Visitor):访问者角色的抽象层,定义了访问者的访问行为visit,参数是被访问者元素,也就是被访问者通过该参数向访问者开放了内部数据结构,比如驱动
- 具体访问者角色(Concrete Visitor):对访问者角色的具体实现,根据实际访问者的逻辑进行实现,比如显卡驱动
- 抽象元素角色(Element):被访问者对象,定义了接收访问者的方法accept,参数是访问者对象,也就是接收访问者对象然后执行一些访问者具体算法实现,比如操作系统
- 具体元素角色(Concrete Element):被访问对象的具体实现,比如Linux操作系统
- 对象结构角色(ObjectStructrue):是一个元素的集合,存放所有的元素,也就是被访问者,可以裂解为一个管理角色,比如以上的例子,可以用它来执行驱动加载的动作,可自由实现
UML:
图片来源:百度图库
2.示例
根据以上的示例,对访问者模式进行简单的实现,示例比较简陋,只为说明访问者模式的原理和思想,
首先声明访问者抽象接口,其中应定义一个visit方法,具体作用就是由被访问者进行回调
/**
* @author twotiger2tigersofast
* @version: 1.0
*/
public interface Driver {
void visit(OperatingSystem os);
}
访问者实现
/**
* @description: 被访问者实现 (显卡)
* @version: 1.0
*/
public class GraphicsDriver implements Driver{
@Override
public void visit(OperatingSystem os) {
System.out.println(os.osName + "=======>加载显卡驱动");
}
}
SoundCard :
/**
* @description: 访问者实现(声卡)
* @version: 1.0
*/
public class SoundCardDriver implements Driver{
@Override
public void visit(OperatingSystem os) {
System.out.println(os.osName + "=======>加载声卡驱动");
}
}
接下来就是被访问者抽象层
/**
* @description: 被访问者抽象 (操作系统)
* @version: 1.0
*/
public abstract class OperatingSystem {
protected String osName;
public OperatingSystem(String osName) {
this.osName = osName;
}
public abstract void accept(Driver driver);
}
对被访者进行实现,被访问者应定义并实现一个accept方法,接收驱动的对象,并在实现中对访问者声明的接口visit方法进行调用,同时将自身数据结构开放给访问者对象。
/**
* @description: 被访问者实现 (Linux)
* @version: 1.0
*/
public class LinuxOS extends OperatingSystem{
public LinuxOS(String osName) {
super(osName);
}
@Override
public void accept(Driver driver) {
driver.visit(this);
}
}
WindowsOS :
/**
* @description: 被访问者实现 (Windows)
* @datetime 2023/4/2 15:38
* @version: 1.0
*/
public class WindowsOS extends OperatingSystem{
public WindowsOS(String osName) {
super(osName);
}
@Override
public void accept(Driver driver) {
driver.visit(this);
}
}
最后提供一个结构对象,对被访问者和访问者进行组织,该对象内部应该持有被访问者对象的集合,并对外提供一个允许访问者调用的方法,总体流程是,客户端通过调用该方法,并将访问者传入,然后由被访问者调用访问者方法,类似一个回调的流程,完成外部具体算法执行流程。
/**
* @description: 结构对象
* @version: 1.0
*/
public class OsStructure {
private List<OperatingSystem> operatingSystems = new ArrayList<>();
public void add(OperatingSystem os){
operatingSystems.add(os);
}
public void loadDriver(Driver driver){
for (OperatingSystem os : operatingSystems){
os.accept(driver);
}
}
}
测试:
/**
* @description: test
* @version: 1.0
*/
public class Test {
public static void main(String[] args) {
OperatingSystem linux = new LinuxOS("Debian");
OperatingSystem windows = new WindowsOS("Windows 11");
Driver graphics = new GraphicsDriver();
Driver soundCard = new SoundCardDriver();
OsStructure structure = new OsStructure();
structure.add(linux);
structure.add(windows);
structure.loadDriver(graphics);
structure.loadDriver(soundCard);
}
}
input:
Debian=======>加载显卡驱动
Windows 11=======>加载显卡驱动
Debian=======>加载声卡驱动
Windows 11=======>加载声卡驱动
以上示例只是一个简单的实现过程,而具体实现依然需要根据实际的情况来决定,实现方式也不是固定的,只要遵循访问者模式的思想即可,
3.总结
访问者模式的优点就是可以在不修改对象结构中元素的情况下,为元素添加新功能,比如驱动发生改变或升级。并且访问者模式可以通过访问者将一些对象结构自身并不关心的行为分离出去,根据功能的不同,划分出不同的访问者。同时访问者模式缺点就是如果对象结构经常发生变化,那访问者的接口和实现都要跟着变化,这个时候维护成本就会变得非常高。试想一下,操作系统加载驱动的方式发生变化,或者一个新的操作系统,那对于所有的驱动来说都要重新适配。此外,还有一个问题就是访问者模式需要被访者开放内部的数据给访问者,一定程度上破坏了封装性。
所以决定是否需要使用访问者模式,最重要的衡量方式之一就是看被访问者是否稳定,如果不稳定,那一定不适合,即使被访问是稳定的,依然需要根据实际的情况,来决定是否使用该模式。