• 作者:老汪软件技巧
  • 发表时间:2024-08-31 15:01
  • 浏览量:

今天我们先来看一个原理看似很简单,但是理解起来有一定难度,使用场景相对较少的行为型模式:访问者模式。

一、模式原理分析

访问者模式的原始定义是:允许在运行时将一个或多个操作应用于一组对象,将操作与对象结构分离。

这个定义会比较抽象,但是我们依然能看出两个关键点:一个是运行时使用一组对象的一个或多个操作,比如,对不同类型的文件(.pdf、.xml、.properties)进行扫描;另一个是分离对象的操作和对象本身的结构,比如,扫描多个文件夹下的多个文件,对于文件来说,扫描是额外的业务操作,如果在每个文件对象上都加一个扫描操作,太过于冗余,而扫描操作具有统一性,非常适合访问者模式。

所以说,访问者模式核心关注点是分离一组对象结构和对象的操作,对象结构可以各不相同,但必须以某一个或一组操作作为连接的中心点。换句话说,访问者模式是以行为(某一个操作)作为扩展对象功能的出发点,在不改变已有类的功能的前提下进行批量扩展。

访问者模式的UML图如下:

从这个 UML 图中,我们能看出访问者模式包含的关键角色有四个。

也就是说,访问者模式可以在不改变各来访类的前提下定义作用于这些来访类的新操作。比如,在没有二维码的时代,旅游景区检票大门提供的访问者实现类就是人工检票,无论哪里来的游客只有购票、检票后才能入园。现在有了二维码后,园区新增一个检票机,将二维码作为一种新的进景点的操作(对应访问者类),那么各类游客(对应访问角色类)可能用支付宝扫二维码,也可能使用微信扫二维码,这时二维码就是新的一个访问点。园区没有改变来访者,但是提供了一种新的操作,对于园区来说,不管你是什么身份的人,对他们来说都只是一个访问者而已。

接下来,我们再来看看 UML 对应的代码实现:

public interface Visitor {
    void visitA(ElementA elementA);
    void visitB(ElementB elementB);
    //...
    //void visitN(ElementN elementN);
}
public class VisitorBehavior implements Visitor {
    @Override
    public void visitA(ElementA elementA) {
        int x = elementA.getAState();
        x++;
        System.out.println("=== 当前A的state为:"+x);
        elementA.setAState(x);
    }
    @Override
    public void visitB(ElementB elementB) {
        double x = elementB.getBState();
        x++;
        System.out.println("=== 当前B的state为:"+x);
        elementB.setBState(x);
    }
}
public interface Element {
    void accept(Visitor v);
}
public class ElementA implements Element {
    private int stateForA = 0;
    public void accept(Visitor v) {
        System.out.println("=== 开始访问元素 A......");
        v.visitA(this);
    }
    public int getAState(){
        return stateForA;
    }
    public void setAState(int value){
        stateForA = value;
    }
}
// 单元测试
public class Demo {
    public static void main(String[] args) {
        List elementList = new ArrayList<>();
        ElementA elementA = new ElementA();
        elementA.setAState(11);
        ElementB elementB = new ElementB();
        elementA.setAState(12);
        elementList.add(elementA);
        elementList.add(elementB);
        for (Element element :elementList) {
            element.accept(new VisitorBehavior());
        }
    }
}
//输出结果

这段代码实现的逻辑比较简单,在单元测试中我们先建立了一个来访类的列表,并依次新建操作访问角色实现类 A 和访问角色实现类 B,将两个对象都加入来访类的列表中,创建相同的访问者实现类 VisitorBehavior,这样就完成了一个访问者模式。

二、为什么使用访问者模式

下面我们再来说说使用访问者模式的原因,主要有以下三个。

第一个,解决编程部分语言不支持动态双分派的能力。比如,Java 是静态多分派、动态单分派的语言。什么叫双分派?所谓双分派技术就是在选择一个方法的时候,不仅要根据消息接收者的运行时来判断,还要根据参数的运行时判断。与之对应的就是单分派,在选择一个方法的时候,只根据消息接收者的运行时来判断。实际上,很多时候我们都无法提前预测所有程序运行的行为,需要在运行时动态传入参数来改变程序的行为,对于 Java 这类语言就需要通过设计模式来弥补这部分功能。

_矩阵的访问_将访问矩阵按行进行划分

第二个,需要动态绑定不同的对象和对象操作。 比如,对不同类型的文件进行扫描、复制并转换新的文件、翻译不同语言文字等,在这一类的场景中,我们往往只是希望在程序运行的过程中进行操作的绑定,用完以后就释放。如果按照传统的方式,每一个新的操作都需要在类上增加方法,不仅得频繁修改代码,而且还会编译打包运行,这些都会非常耗时,这时使用访问者模式就能很好地解决这个问题。

第三个,通过行为与对象结构的分离,实现对象的职责分离,提高代码复用性。访问者模式能够在对象结构复杂的情况下动态地为对象添加操作,这就做到了对象的职责分离,尤其对于一些老旧的系统来说,能够快速地扩展功能,提高代码复用性。

三、使用场景分析

假设你是一家路由器软件的生产商,接了很多家不同硬件品牌的路由器的软件需求(比如,D-Link、TP-Link),这时你需要针对不同的操作系统(比如,Linux 和 Windows)做路由器发送数据的兼容功能。于是,你先定义了路由器的访问角色类 Router,如下代码:

public interface Router {
    void sendData(char[] data);
    void accept(RouterVisitor v);
}

然后,再分别针对不同型号的路由器具体实现对应的功能。

public class DLinkRouter implements Router{
    @Override
    public void sendData(char[] data) {
    }
    @Override
    public void accept(RouterVisitor v) {
        v.visit(this);
    }
}
public class TPLinkRouter implements Router {
    @Override
    public void sendData(char[] data) {
    }
    @Override
    public void accept(RouterVisitor v) {
        v.visit(this);
    }
}

接下来,再配置一下访问者类 RouterVisitor、访问者实现类 LinuxRouterVisitor 和 WindowsRouterVisitor,用于给不同的路由器提供访问的入口点。

public interface RouterVisitor {
    void visit(DLinkRouter router);
    void visit(TPLinkRouter router);
}
public class LinuxRouterVisitor implements RouterVisitor{
    @Override
    public void visit(DLinkRouter router) {
        System.out.println("=== DLinkRouter Linux visit success!");
    }
    @Override
    public void visit(TPLinkRouter router) {
        System.out.println("=== TPLinkRouter Linux visit success!");
    }
}
public class WindowsRouterVisitor implements RouterVisitor{
    @Override
    public void visit(DLinkRouter router) {
        System.out.println("=== DLinkRouter Windows visit success!");
    }
    @Override
    public void visit(TPLinkRouter router) {
        System.out.println("=== DLinkRouter Windows visit success!");
    }
}

到此就完成了所有配置,最后再运行一下测试:

public class Client {
    public static void main(String[] args) {
        LinuxRouterVisitor linuxRouterVisitor = new LinuxRouterVisitor();
        WindowsRouterVisitor windowsRouterVisitor = new WindowsRouterVisitor();
        DLinkRouter dLinkRouter = new DLinkRouter();
        dLinkRouter.accept(linuxRouterVisitor);
        dLinkRouter.accept(windowsRouterVisitor);
        TPLinkRouter tpLinkRouter = new TPLinkRouter();
        tpLinkRouter.accept(linuxRouterVisitor);
        tpLinkRouter.accept(windowsRouterVisitor);
    }
}
//输出结果
=== DLinkRouter Linux visit success!
=== DLinkRouter Windows  visit success!
=== TPLinkRouter Linux visit success!
=== DLinkRouter Windows  visit success!

从这个示例我们可以发现,不同型号的路由器可以在运行时动态添加(第一次分派),对于不同的操作系统来说,路由器可以动态地选择适配(第二次分派),整个过程完成了两次动态绑定。这也就引出了访问者模式常用的场景;

所以说,访问者模式重点关注不同类型对象在运行时动态进行绑定,以及对多个对象增加统一操作的场景。

四、访问者模式的优缺点

通过上面的分析,我们也可以得出访问者模式主要有以下优点。

当然,访问者模式同样不是万能的,它也有一些缺点。