当前位置:首页 > 技术 > 正文内容

设计模式之访问者模式

Lotus2022-10-06 19:05技术

大多数情况下你不需要访问者模式,但当一旦需要访问者模式时,那就是真的需要它了,这是设计模式创始人的原话。可以看出应用场景比较少,但需要它的时候是不可或缺的,这篇文章就开始学习最后一个设计模式——访问者模式。

一、概念理解

访问者模式概念:封装作用于某对象结构中的各元素的操作,它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

通俗的解释就是,系统中有一些固定结构的对象(元素),在其内部提供一个accept()方法用来接受访问者对象的访问,不同的访问者对同一元素的访问内容不同,所以使得相同的元素可以产生不同的元素结果。

比如在一个人事管理系统中,有多个工种的员工和多个老板,不同的老板对同一个员工的关注点是不同的,CTO可能关注的就是技术,CEO可能更注重绩效。

员工就是一个稳定的元素,老板就是变化的,对应概念就是:封装员工的一些操作,可以在不改变员工类的前提下,增加新的老板访问同一个员工。

在访问者模式中包含五个角色,抽象元素、具体元素、抽象访问者、具体访问者、结构元素。

抽象元素:定义一个接受访问的方法accept,参数为访问者对象。

具体元素:提供接受访问者访问的具体实现调用访问者的访问visit,并定义额外的数据操作方法。

抽象访问者:这个角色主要是定义对具体元素的访问方法visit,理论上来说方法数等于元素(固定类型的对象,也就是被访问者)个数。

具体访问者:实现对具体元素的访问visit方法,参数就是具体元素。

结构对象:创建一个数组用来维护元素,并提供一个方法访问所有的元素。

二、案例实现

在一个公司有干活的工程师和管理者,也有抓技术的CTO和管绩效的CEO,CTO和CEO都会访问管理员和工程师,当公司来了新的老板,只需要增加访问者即可。

工程师和管理者就是元素、公司就是结构体、CEO、CTO就是访问者。

抽象元素:

/**
 *  员工 抽象元素 被访问者
 * @author tcy
 * @Date 29-09-2022
 */
public interface ElementAbstract {
    void accept(VisitorAbstract visitor);
}

具体元素-工程师:

/**
 * 工程师 具体元素 被访问者
 * @author tcy
 * @Date 29-09-2022
 */
public class ElementEngineer implements ElementAbstract {

    private String name;

    private int kpi;

    ElementEngineer(String name){
        this.name = name;
        this.kpi = new Random().nextInt(10);
    }

    public String getName() {
        return name;
    }

    public int getKpi() {
        return kpi;
    }


    @Override
    public void accept(VisitorAbstract visitor) {
        visitor.visit(this);
    }

    public int getCodeLineTotal(){
        return this.kpi * 1000000;
    }
}

具体元素-管理者:

/**
 * 管理者 具体元素 被访问者
 * @author tcy
 * @Date 29-09-2022
 */
public class ElementManager implements ElementAbstract {
    private String name;

    private int kpi;

    ElementManager(String name){
        this.name = name;
        this.kpi = new Random().nextInt(10);
    }

    public String getName() {
        return name;
    }

    public int getKpi() {
        return kpi;
    }

    @Override
    public void accept(VisitorAbstract visitor) {
        visitor.visit(this);
    }

    public int getProductNum(){
        return this.kpi * 10;
    }

}

抽象访问者:

/**
 * 抽象访问者
 * @author tcy
 * @Date 29-09-2022
 */
public interface VisitorAbstract {
    void visit(ElementEngineer engineer);

    void visit(ElementManager manager);
}

具体访问者-CEO

/**
 * 具体访问者CEO
 * @author tcy
 * @Date 29-09-2022
 */
public class VisitorCEO implements VisitorAbstract {
    @Override
    public void visit(ElementEngineer engineer) {
        System.out.println("工程师:" + engineer.getName() + "KPI:" + engineer.getKpi());
    }

    @Override
    public void visit(ElementManager manager) {
        System.out.println("经理:" + manager.getName() + "KPI:" + manager.getKpi() + " 今年共完成项目:" + manager.getProductNum() + "个");
    }
}

具体访问者-CTO

/**
 * 具体访问者CTO
 * @author tcy
 * @Date 29-09-2022
 */
public class VisitorCTO implements VisitorAbstract {
    @Override
    public void visit(ElementEngineer engineer) {
        System.out.println("工程师:" + engineer.getName() + " 今年代码量" + engineer.getCodeLineTotal() + "行");
    }

    @Override
    public void visit(ElementManager manager) {
        System.out.println("经理:" + manager.getName() + " 今年共完成项目:" + manager.getProductNum() + "个");
    }
}

结构体:

/**
 * 结构对象
 * @author tcy
 * @Date 29-09-2022
 */
public class Structure {
    List<ElementAbstract> list = new ArrayList<>();

    public Structure addEmployee(ElementAbstract employee){
        list.add(employee);
        return this;
    }

    public void report(VisitorAbstract visitor){
        list.forEach(employee -> {
            employee.accept(visitor);
        });
    }

}

客户端:

/**
 * @author tcy
 * @Date 29-09-2022
 */
public class Client {
    public static void main(String[] args) {
        //元素对象
        ElementEngineer engineerZ = new ElementEngineer("小张");
        ElementEngineer engineerW = new ElementEngineer("小王");
        ElementEngineer engineerL = new ElementEngineer("小李");

        ElementManager managerZ = new ElementManager("张总");
        ElementManager managerW = new ElementManager("王总");
        ElementManager managerL = new ElementManager("李总");

        //结构体对象
        Structure structure = new Structure();
        structure.addEmployee(engineerZ).addEmployee(engineerW).addEmployee(engineerL).addEmployee(managerZ).addEmployee(managerW).addEmployee(managerL);
        structure.report(new VisitorCTO());
        System.out.println("---------------------------------------");
        structure.report(new VisitorCEO());


    }
}

访问者不愧是最难的设计模式,方法间的调用错综复杂,日常开发的使用频率很低,很多程序员宁可代码写的麻烦一点也不用这种设计模式,但是作为学习者就要学习各种设计模式了。

三、访问者模式在JDk中的应用

JDK的NIO中的 FileVisitor 接口采用的就是访问者模式。

在早期的 Java 版本中,如果要对指定目录下的文件进行遍历,必须用递归的方式来实现,这种方法复杂且灵活性不高。

Java 7 版本后,Files 类提供了 walkFileTree() 方法,该方法可以很容易的对目录下的所有文件进行遍历,需要 Path、FileVisitor 两个参数。其中,Path 是要遍历文件的路径,FileVisitor 则可以看成一个文件访问器,源码如下。

image-20220930092623291

FileVisitor 主要提供了 4 个方法,且返回结果的都是 FileVisitResult 对象值,用于决定当前操作完成后接下来该如何处理。FileVisitResult 是一个枚举类,代表返回之后的一些后续操作,源码如下。

image-20220929150014824

FileVisitResult 主要包含 4 个常见的操作。

  • FileVisitResult.CONTINUE:这个访问结果表示当前的遍历过程将会继续。
  • FileVisitResult.SKIP_SIBLINGS:这个访问结果表示当前的遍历过程将会继续,但是要忽略当前文件/目录的兄弟节点。
  • FileVisitResult.SKIP_SUBTREE:这个访问结果表示当前的遍历过程将会继续,但是要忽略当前目录下的所有节点。
  • FileVisitResult.TERMINATE:这个访问结果表示当前的遍历过程将会停止。

通过访问者去遍历文件树会比较方便,比如查找文件夹内符合某个条件的文件或者某一天内所创建的文件,这个类中都提供了相对应的方法。它的实现也非常简单,代码如下。

image-20220929150056647

在JDK的应用中我们提供的文件就看做是一个稳定元素,对应访问者模式中的抽象元素;而Files.walkFileTree()方法中的FileVisitor 参数就可看做是角色中的访问者。

四、访问者模式中的伪动态双分派

访问者模式中有一个重要的概念叫:伪动态双分派。

我们一步一步解读它的含义,什么叫分派?根据对象的类型而对方法进行的选择,就是分派(Dispatch)。

发生在编译时的分派叫静态分派,例如重载(overload),发生在运行时的分派叫动态分派,例如重写(overwrite)。

其中分派又分为单分派和多分派。

单分派:依据单个变量进行方法的选择就叫单分派,Java 动态分派(重写)只根据方法的接收者一个变量进行分配,所以其是单分派。

多分派:依据多个变量进行方法的选择就叫多分派,Java 静态分派(重载)要根据方法的接收者与参数这两个变量进行分配,所以其是多分派。

理解了概念我们接着看我们的案例:

 @Override
    public void accept(VisitorAbstract visitor) {
        visitor.visit(this);
    }

我们案例中的accept方法,是由元素的运行时类型决定的,应该是属于动态单分派。

我们接着看 visitor.visit(this)又是一次动态单分派,两次动态单分派实现了双分派的效果,所以称为伪动态双分派。

这个概念理解就好,实际应用中知不知道这玩意都不影响。

五、总结

当你有个类,里面的包含各种类型的元素,这个类结构比较稳定,不会经常增删不同类型的元素。而需要经常给这些元素添加新的操作的时候,考虑使用此设计模式。

适用对象结构比较稳定每增加一个元素访问者都要大变动,但加新的操作很简单。集中相关的操作、分离无关的操作。

缺点只有两个字-复杂,号称是最复杂的设计模式。

设计模式的学习要成体系,推荐你看我往期发布的设计模式文章。

一、设计模式概述

二、设计模式之工厂方法和抽象工厂

三、设计模式之单例和原型

四、设计模式之建造者模式

五、设计模式之代理模式

六、设计模式之适配器模式

七、设计模式之桥接模式

八、设计模式之组合模式

九、设计模式之装饰器模式

十、设计模式之外观模式

十一、外观模式之享元模式

十二、设计模式之责任链模式

十三、设计模式之命令模式

十四、设计模式之解释器模式

十五、设计模式之迭代器模式

十六、设计模式之中介者模式

十七、设计模式之备忘录模式

十八、设计模式之观察者模式

十九、设计模式之状态模式

二十、设计模式之策略模式

二十一、设计模式之模板方法模式

扫描二维码推送至手机访问。

版权声明:本文来源于网络,仅供学习,如侵权请联系站长删除。

本文链接:https://news.layui.org.cn/post/137.html

分享给朋友:

“设计模式之访问者模式” 的相关文章

二叉树两个节点的最近公共祖先问题

二叉树两个节点的最近公共祖先问题 作者:Grey 原文地址: 博客园:二叉树两个节点的最近公共祖先问题 CSDN:二叉树两个节点的最近公共祖先问题 题目描述 给定一棵二叉树的头节点 head,和另外两个节点 a 和 b , 返回 a 和 b 的最低公共祖先。 题目链接见:LeetCode 236. Lowest Common Ancestor of a Binary Tree 主要思路: 本题也是...

一篇文章带你掌握主流服务层框架——SpringMVC

一篇文章带你掌握主流服务层框架——SpringMVC 在之前的文章中我们已经学习了Spring的基本内容,SpringMVC隶属于Spring的一部分内容 但由于SpringMVC完全针对于服务层使用,所以我们在介绍时常常把SpringMVC单独当作一个大章节来学习 温馨提醒:在学习SpringMVC前请确保已学习Spring内容 SpringMVC简介 首先我们先来简单了解一下SpringM...

国庆节,零代码帮你搞定假期美食菜单

当国庆假期遇上美食 每一口都唇齿留香 特色美食太多,不知道吃什么? AppCube带你一分钟搞定假期美食 来一场舌尖上的旅行 零代码,让假期生活有滋有味 国庆小长假,三五好友结伴出游,最纠结的莫过于中午吃什么,晚上吃什么?翻翻攻略,当地特色美食令人眼花缭乱……体验通过AppCube设计一款“国庆假期美食菜单收集”应用,解决大家的选择困难症。 基于AppCube零代码能力,小白也能DIY应用开发...

十分钟速成DevOps实践

摘要:以华为云软件开发平台DevCloud为例,十分钟简单体验下DevOps应用上云实践——H5经典小游戏上云。 本文分享自华为云社区《​​《DevOps实践秘籍》十分钟速成DevOps实践​​》,作者:AppCloud小助手 。 DevOps是什么? DevOps是Development和Operations的组合词,简单点理解就是研发运维一体化的方法论,目的是通过自动化“软件交付”和“架构变...

Golang依赖包的各项指标分析总结

专注于PHP、MySQL、Linux和前端开发,感兴趣的感谢点个关注哟!!!​​文章已收录​​,主要包含的技术有PHP、Redis、MySQL、JavaScript、HTML&CSS、Linux、Java、Golang、Linux和工具资源等相关理论知识、面试题和实战内容。 文章导读 在日常开发中,我们在自己的代码库中难免都会引入外部的包,或者公司内部的私有包。在引入这些包时,我们一般都...

分布式存储系统之Ceph集群存储池操作

  前文我们了解了ceph的存储池、PG、CRUSH、客户端IO的简要工作过程、Ceph客户端计算PG_ID的步骤的相关话题,回顾请参考https://www.cnblogs.com/qiuhom-1874/p/16733806.html;今天我们来聊一聊在ceph上操作存储池相关命令的用法和说明;   在ceph上操作存储池不外乎就是查看列出、创建、重命名和删除等操作,常用相关的工具都是“cep...

发表评论

访客

看不清,换一张

◎欢迎参与讨论,请在这里发表您的看法和观点。