首页 > 其他分享 >【设计模式】访问者模式Visitor:实现对象级别的矩阵结构

【设计模式】访问者模式Visitor:实现对象级别的矩阵结构

时间:2023-09-16 16:06:22浏览次数:32  
标签:对象 Visitor void visit 操作 设计模式 public 访问者

(目录)


访问者模式一个原理看似很简单,但是理解起来有一定难度,使用场景相对较少的行为型模式: 它能将算法与其所作⽤的对象隔离开来

image-20230916144227172

假如有这样⼀位⾮常希望赢得新客户的资深保险代理⼈。 他可以拜访街区中的每栋楼, 尝试向每个路⼈ 推销保险。

所以, 根据⼤楼内组织类型的不同, 他可以提供专⻔的保单:

  • 如果建筑是居⺠楼, 他会推销医疗保险
  • 如果建筑是银⾏, 他会推销失窃保险
  • 如果建筑是咖啡厅, 他会推销⽕灾和洪⽔保险。

模式原理

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

这个定义非常的抽象,但是依然要找出其关键点,有两个

  1. 运行时使用一组对象的一个或多个操作比如。对不同类型的文件(.pdf、.xml、.properties)进行扫描;
  2. 分离对象的操作和对象本身的结构,比如,扫描多个文件夹下的多个文件,对于文件来说,扫描是额外的业务操作,如果在每个文件对象上都加一个扫描操作,太过于冗余,而扫描操作具有统一性,非常适合访问者模式。

由此可见,访问者模式核心关注点是:

分离一组对象结构和对象的操作,对象结构可以各不相同,但必须以某一个或一组操作作为连接的中心点。

换句话说,访问者模式是以行为(某一个操作)作为扩展对象功能的出发点,在不改变已有类的功能的前提下进行批量扩展。

访问者模式的 UML 图:

从上图中,可以看出访问者模式包含的关键角色有四个

  1. 访问者类(Visitor):一个接口或抽象类,定义声明所有可访问类的访问操作;
  2. 访问者实现类(VisitorBehavior):实现在访问者类中声明的所有访问方法;
  3. 访问角色类(Element):定义一个可以获取访问操作的接口,这是使客户端对象能够“访问”的入口点;
  4. 访问角色实现类(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,这样就完成了一个访问者模式。

使用访问者模式的原因

使用访问者模式的原因,主要有以下三个:

  1. 解决编程部分语言不支持动态双分派的能力。比如:
  • Java 是静态多分派、动态单分派的语言。什么叫双分派?所谓双分派技术就是在选择一个方法的时候,不仅要根据消息接收者的运行时来判断,还要根据参数的运行时判断。与之对应的就是单分派,在选择一个方法的时候,只根据消息接收者的运行时来判断;
  • 实际上,大多数时候我们都无法提前预测所有程序运行的行为,需要在运行时动态传入参数来改变程序的行为,对于 Java 这类语言就需要通过设计模式来弥补这部分功能。
  1. 需要动态绑定不同对象和对象操作。 比如,对不同类型的文件进行扫描、复制并转换新的文件、翻译不同语言文字等,在这类场景中,只是希望在程序运行过程中进行操作绑定,用完以后就释放。如果按照传统的方式,每个新操作都需要在类上增加方法,不仅得频繁修改代码,而且还会编译打包运行,这些都会非常耗时,这时使用访问者模式就能很好地解决这个问题;
  2. 通过行为与对象结构的分离,实现对象的职责分离,提高代码复用性。访问者模式能够在对象结构复杂的情况下动态地为对象添加操作,这就做到了对象的职责分离,尤其对于一些老旧的系统来说,能够快速地扩展功能,提高代码复用性。

使用场景分析

通过不同路由访问不同操作系统的例子来说明。

假设一家路由器软件的生产商,接了很多家不同硬件品牌的路由器的软件需求(比如,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!

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

  1. 当对象的数据结构相对稳定,而操作却经常变化的时候。 比如,上面例子中路由器本身的内部构造(也就是数据结构)不会怎么变化,但是在不同操作系统下的操作可能会经常变化,比如,发送数据、接收数据等;
  2. 需要将数据结构与不常用的操作进行分离的时候。 比如,扫描文件内容这个动作通常不是文件常用的操作,但是对于文件夹和文件来说,和数据结构本身没有太大关系(树形结构的遍历操作),扫描是一个额外的动作,如果给每个文件都添加一个扫描操作会太过于重复,这时采用访问者模式是非常合适的,能够很好分离文件自身的遍历操作和外部的扫描操作;
  3. 需要在运行时动态决定使用哪些对象和方法的时候。 比如,对于监控系统来说,很多时候需要监控运行时的程序状态,但大多数时候又无法预知对象编译时的状态和参数,这时使用访问者模式就可以动态增加监控行为。

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


优缺点

访问者模式主要有以下优点:

  1. 简化客户端操作。比如,扫描文件时,对于客户端来说只需要执行扫描,而不需要关心不同类型的文件该怎么读取,也不用知道文件该如何被读取;
  2. 增加新的访问操作和访问者会非常便捷。在每次新增操作时,都是将对象视为统一的访问者,只需要关注操作是否正确地在对象上执行,而不需要关注对象如何被构建,这样使操作变得更加容易;
  3. 满足开闭原则。由于访问者模式没有对原有对象进行修改,只是新增了外部的统一操作,扩展新类不影响旧类,满足开闭原则;
  4. 满足单一职责原则。每一个行为都是一个单一的行为操作,能够组合相关类的功能逻辑,到代码内聚度更高;
  5. 通过行为能够快速组合一组复杂的对象结构。比如,访问角色类可能是树形结构,增加一个新操作后,对每一个对象都是进行的统一操作,这时新增一个其他结构的对象也能按照统一操作进行,便能将多个不同结构的对象进行自由组合。

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

  1. 增加新的数据结构困难。因为新增结构,又需要新增操作,这时就让结构和操作发生关联,也就破坏了原有的模式,并且可能造成原有结构的不可用;
  2. 具体元素在变更时需要修改代码,容易引入问题。虽然访问者模式分离了操作和对象,但是当访问者对象本身发生变化时,依然需要修改代码,这时可能会对操作的方法造成影响。

总结

访问者模式能够通过添加新的行为来封装不同类型的对象,并隐藏不同对象各自的变化。这里所说的隐藏的变化主要包括:

  • 允许添加新行为到一组对象里;
  • 行为的实现和数量;
  • 在运行时动态给对象添加额外行为;
  • 访问者类和访问角色类之间的调用关系。

访问者模式的原理理解起来不像之前结构型和创建型模式那么容易,主要原因在于行为的动态多变性,涉及很多编译原理和操作系统的知识。说实话,访问者模式在真实的应用中算得上是比较难的模式之一了。


标签:对象,Visitor,void,visit,操作,设计模式,public,访问者
From: https://blog.51cto.com/panyujie/7493909

相关文章

  • 【设计模式】解释器模式Interpreter Pattern:实现自定义配置规则功能
    解释器模式使用频率不算高,**通常用来描述如何构建一个简单“语言”的语法解释器。**它只在一些非常特定的领域被用到,比如:编译器;规则引擎;正则表达式;SQL解析等。不过,了解它的实现原理,可以帮助思考如何通过更简洁的规则来表示复杂的逻辑。模式原理分析解释器模式的原始......
  • 使用设计模式改写if/else或switch/case语句
    在写代码的时候,经常会用到if/else语句或者switch/case语句。虽然很省事,但是没有体现到java的封装、继承、多态等特性。没有用到java的面向对象编程的精髓。比如这种if/else语句:Stringstr="菠萝";if("苹果".equals(str)){ System.out.println("又大又红的苹果");}else......
  • 8.前端设计模式之混合模式
    MaxinPattern: Addfunctionalitytoobjectsorclasseswithoutinheritance在不使用的继承的情况下为对象或者类添加功能在JavaScript中混合也是通过原型链实现的。比如有个Dog类:classDog{constructor(name){this.name=name;}}现在我们希望为Dog类添加bark、......
  • 在工作流引擎设计领域,是否自动计算未来的处理人的设计模式有哪些?
    概述流程的第一个节点发送下去的时候,就要把以后所有节点的处理人计算出来,能清楚的知道每个节点都是那些人处理.计算未来处理人包括抄送节点、与待办节点.默认的模式为:每个节点发送的时候即使计算,就是不计算未来处理人.流程设计特征.流程的所有节点的接受人不能是主管选择的,只能......
  • 设计模式 C++
    (设计模式)(李建忠C++)23种设计模式组件协作模板方法父类中定义组件(函数)的调用流程,每个组件使用虚函数进行实现,然后子类中可以重写父类中虚函数的实现。如果我们发现一个算法的组件(函数)的调用流程都是一样的,但是步骤中的各个组件的实现可能有所差异,此时会使用模板方法。【......
  • 软件设计模式系列之六——单例模式
    1模式的定义单例模式(SingletonPattern)是一种常见的创建型设计模式,其主要目的是确保一个类只有一个实例,并提供一个全局访问点来获取该实例。这意味着无论何时何地,只要需要该类的实例,都会返回同一个实例,而不是创建多个相同的实例。单例模式通常用于管理全局状态、资源共享或限制......
  • 趣解设计模式之《小店儿菜单的故事》
    〇、小故事在一座小镇上,有两家特别有名气的小店,一家是远近闻名的早餐店,它家的早餐特别好吃,每天早上都能排起长长的队伍;另一家是个蛋糕店,他家是专门从法国请来的蛋糕师傅,蛋糕的香味真是香飘万里。自从听说这个酱香拿铁销量非常高之后,这两家店的老板们就觉得强强联合才是能让小店更......
  • 常用设计模式
    一、工厂模式+单例模式使用工厂模式来创建对象,通过单例模式来保证该工厂只有一个实例,从而减少创建对象时的开销。首先,创建一个工厂类,该类使用单例模式来保证只有一个实例,该实例负责创建对象。然后,根据需要创建多个工厂方法,每个方法用于创建不同的对象。classSingletonFacto......
  • 7.前端设计模式之模块模式
    将代码拆分成更小更易复用的代码块 JavaScript从ES2015开始支持模块化,一个文件就是一个模块,以前也可以把一个大的JS文件拆分到多个文件里,但容易出现定义的变量、函数名称冲突,文件多了依赖关系也不好管理,上线时觉得a.js文件没什么用就没发布,直到某个功能出现xxxisnotafunction......
  • 软件设计模式系列之五——建造者模式
    1模式的定义建造者模式是一种对象创建型设计模式,它将一个复杂对象的构建过程与其表示分离。这意味着你可以使用相同的构建过程来创建不同类型的对象,而不必关心每个对象的内部细节。这种模式适用于构建具有复杂配置的对象,例如具有多个可选组件的汽车。2结构建造者模式的结构包......