首页 > 其他分享 >访问者模式:如何实现对象级别的矩阵结构?

访问者模式:如何实现对象级别的矩阵结构?

时间:2024-08-29 23:24:14浏览次数:12  
标签:对象 visit void 矩阵 操作 级别 public 访问者

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

一、模式原理分析

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

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

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

访问者模式的UML图如下:

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

  • 访问者类(Visitor):这是一个接口或抽象类,定义声明所有可访问类的访问操作。

  • 访问者实现类(VisitorBehavior):实现在访问者类中声明的所有访问方法。

  • 访问角色类(Element):定义一个可以获取访问操作的接口,这是使客户端对象能够“访问”的入口点。

  • 访问角色实现类(Element A 等):实现访问角色类接口的具体实现类,将访问者对象传递给此对象作为参数。

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

接下来,我们再来看看 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<Element> 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!

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

  • 当对象的数据结构相对稳定,而操作却经常变化的时候。 比如,上面例子中路由器本身的内部构造(也就是数据结构)不会怎么变化,但是在不同操作系统下的操作可能会经常变化,比如,发送数据、接收数据等。

  • 需要将数据结构与不常用的操作进行分离的时候。 比如,扫描文件内容这个动作通常不是文件常用的操作,但是对于文件夹和文件来说,和数据结构本身没有太大关系(树形结构的遍历操作),扫描是一个额外的动作,如果给每个文件都添加一个扫描操作会太过于重复,这时采用访问者模式是非常合适的,能够很好分离文件自身的遍历操作和外部的扫描操作。

  • 需要在运行时动态决定使用哪些对象和方法的时候。 比如,对于监控系统来说,很多时候需要监控运行时的程序状态,但大多数时候又无法预知对象编译时的状态和参数,这时使用访问者模式就可以动态增加监控行为。

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

四、访问者模式的优缺点

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

  • 简化客户端操作。比如,扫描文件时,对于客户端来说只需要执行扫描,而不需要关心不同类型的文件该怎么读取,也不用知道文件该如何被读取。

  • 增加新的访问操作和访问者会非常便捷。在每次新增操作时,都是将对象视为统一的访问者,只需要关注操作是否正确地在对象上执行,而不需要关注对象如何被构建,这样使操作变得更加容易。

  • 满足开闭原则。由于访问者模式没有对原有对象进行修改,只是新增了外部的统一操作,扩展新类不影响旧类,满足开闭原则。

  • 满足单一职责原则。每一个行为都是一个单一的行为操作,能够组合相关类的功能逻辑,到代码内聚度更高。

  • 通过行为能够快速组合一组复杂的对象结构。比如,访问角色类可能是树形结构,增加一个新操作后,对每一个对象都是进行的统一操作,这时新增一个其他结构的对象也能按照统一操作进行,便能将多个不同结构的对象进行自由组合。

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

  • 增加新的数据结构困难。因为新增结构,又需要新增操作,这时就让结构和操作发生关联,也就破坏了原有的模式,并且可能造成原有结构的不可用。

  • 具体元素在变更时需要修改代码,容易引入问题。虽然访问者模式分离了操作和对象,但是当访问者对象本身发生变化时,依然需要修改代码,这时可能会对操作的方法造成影响。

文章(专栏)将持续更新,欢迎关注公众号:服务端技术精选。欢迎点赞、关注、转发

个人小工具程序上线啦,通过公众号菜单即可体验,欢迎大家体验后提出优化意见

标签:对象,visit,void,矩阵,操作,级别,public,访问者
From: https://blog.51cto.com/jiangyi/11870527

相关文章

  • 代码随想录算法训练营,29日 | 704. 二分查找,27. 移除元素,977.有序数组的平方,209.长度最
    数组基础文档讲解︰代码随想录(programmercarl.com)1.连续空间、相同类型元素2.元素只能覆盖3.二维数组的地址连续吗(C++连续,Java不连续)704.二分查找题目链接:704.二分查找文档讲解︰代码随想录(programmercarl.com)视频讲解︰二分查找日期:2024-08-29思路:第一反应是想到二分查......
  • SQLserver中的事务以及数据并发的问题和事务的四种隔离级别
    SQLserver中的事务在SQLServer中,事务是一组原子性的SQL语句集合,要么全部成功执行,要么全部不执行。事务确保数据库的完整性和一致性,即使在发生错误或系统故障的情况下也是如此。SQLServer支持本地事务和分布式事务。事务的特性(ACID属性)原子性(Atomicity):事务中的所有......
  • Day07_0.1基础学习MATLAB学习小技巧总结(7)——矩阵算数运算
    利用暑假的时间把碎片化的MATLAB知识重新系统的学习一遍,为了在这个过程中加深印象,也为了能够有所足迹,我会把自己的学习总结发在专栏中,以便学习交流。素材来源“数学建模清风”特此说明:本博客的内容只在于总结在使用matlab中的一些小技巧,并非教程,若想系统的学习MATLAB,也可以移......
  • MySQL的四种事务隔离级别
    本文实验的测试环境:Windows10+cmd+MySQL5.6.36+InnoDB一、事务的基本要素(ACID)1、原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的......
  • 访问者模式详解
    访问者模式简介:类的内部结构不变的情况下,不同的访问者访问这个对象都会呈现出不同的处理方式。人话:其实就是为了解决类结构不变但操作处理逻辑易变的问题,把对数据的操作都封装到访问者类中,我们只需要调用不同的访问者,而无需改变改变结构类,实现了。举个理发店的例子......
  • 乘法|python矩阵基本运算(学习笔记二)
    在前述文章中,我们已经知道,python通过使用numpy模块,创建矩阵形数组至少可以采用两种方法。也即,通过array和matrix子模块分别创建,详情请参考以下链接。https://blog.csdn.net/weixin_44855046/article/details/141564179?spm=1001.2014.3001.5502进一步,上述链接指向文章也通过测......
  • 最大矩阵区间 题解
    题意简述给定\(n\)行\(m\)列矩阵\(A\)。对于每一行\(i\),选择非空区间\([l_i,r_i]\),满足\(\foralli\in[1,n)\),\([l_i,r_i]\)和\([l_{i+1},r_{i+1}]\)相交,即\(\max\{l_i,l_{i+1}\}\leq\min\{r_i,r_{i+1}\}\)。求所有选出区间的\(A_{i,j}\)值......
  • 加减法| python矩阵运算(学习笔记一)
    python的数学运算部分基本都在使用numpy模块,如果初次使用python,安装该模块最简单的办法就是:搜索框输入cmd打开命令提示符,输入以下代码等待安装即可。pipinstallnumpy如果不确定是否安装好,打开pycharm(此处默认为已经安装该软件),输入以下代码:importnumpyasnp之后即可定......
  • 搜索二维矩阵 II(LeetCode)
    题目编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:每行的元素从左到右升序排列。每列的元素从上到下升序排列。解题"""时间复杂度为O(m+n),其中m是矩阵的行数,n是矩阵的列数。"""defsearchMatrix(matrix,t......
  • 【从头写CAD】 转换矩阵类系列一,总说明及泛型类
    /*矩阵类编程思路总说明:平面CAD对象主要包括点(point)、线(line含线段、直线、射线,宽线、多段线)、平面形状(shap含矩形、圆形、椭圆、文字、图块实体、外部参照实体及各种标注等)。我们先用点(point)来说明矩阵功能。点(P),可以用向量(1,x,y)表示。一、如果点发生平移......