设计模式——2_A 访问者(Visitor)

文章目录

  • 定义
  • 图纸
  • 一个例子:如何给好奇宝宝提供他想知道的内容
    • 菜单、菜品和配方
          • Menu(菜单) & Cuisine(菜品)
          • Material(物料、食材)
    • 产地、有机蔬菜和卡路里
          • Cuisine & Material
    • 访问者
          • Visitor
          • Cuisine & Material
  • 碎碎念
    • 访问者和双分派
    • 访问者和代理
    • 写在最后的碎碎念

定义

表示一个作用于某对象结构中的个元素的操作。他使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作


访问器和其他的设计模式一样,致力于将程序中的 变化不变 的部分剥离,至于是谁被独立出来,这不好说。像策略状态 这种模式是将变化独立出来;而也有像 迭代器模板方法 这样将不变的部分独立出来的。总之袋子里只有两种苹果,你拿走青色的,剩下的都是红色的,反之亦然

可访问器又是设计模式中的异类。在其他的设计模式中,我们总是强调 隐藏细节、依赖抽象。但访问器反其道而行之,他是23种基础设计模式中唯一一个要求 被作用方,也就是 被访问者,必须对 访问者 公开自己的细节,而且访问者会依赖具体类,也就是说访问者的复杂程度是会随着你对被访问者类簇的拓展而复杂化的




图纸

在这里插入图片描述




一个例子:如何给好奇宝宝提供他想知道的内容

某天,你发现的出生点居然是大洋彼岸的美利坚,正当你准备掐掐自己人中看看是不是还没醒的时候,肚子却提醒你该补充能量了。你坚信有一技傍身的人总是饿不死的,于是准备靠着祖传的川菜手艺在唐人街创出一片天地。摸爬滚打几年后,随着一串鞭炮被点燃,属于你的川菜馆终于开张,可是当你准备做一个电子菜单的时候却犯了愁

客人们恨不得了解自己将点的菜的全部信息,而你却不能公开自己赖以生存的秘方,这就是我们这次的例子(没错,前面那个浪迹美国的感人故事跟正文毫无关联)

准备好了吗?四人组圣经里的最后一个设计模式的例子也开始了:



菜单、菜品和配方

为了展示菜单,无论如何你需要一个和菜品相关的类簇,就像这样:

在这里插入图片描述

Menu(菜单) & Cuisine(菜品)
/**
 * 菜品
 */
public class Cuisine {

    /**
     * 菜品名
     */
    private String name;

    /**
     * 配料表
     */
    private List<Material> burdenSheet;

    public Cuisine(String name, List<Material> burdenSheet) {
        this.name = name;
        this.burdenSheet = burdenSheet;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setBurdenSheet(List<Material> burdenSheet) {
        this.burdenSheet = burdenSheet;
    }
}

/**
 * 菜单
 */
public class Menu {

    /**
     * 菜品列表
     */
    private List<Cuisine> cuisineList;

    public static Menu createMenu(){
        Menu menu = new Menu();
        //初始化cuisineList的动作
        return menu;
    }

    private Menu() {
    }
}
Material(物料、食材)
/**
 * 食材
 */
public class Material {

    /**
     * 食材名
     */
    private String name;
    /**
     * 辛辣度
     */
    private int spicyDegree;
    /**
     * 咸度
     */
    private int saltyDegree;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getSpicyDegree() {
        return spicyDegree;
    }

    public void setSpicyDegree(int spicyDegree) {
        this.spicyDegree = spicyDegree;
    }

    public int getSaltyDegree() {
        return saltyDegree;
    }

    public void setSaltyDegree(int saltyDegree) {
        this.saltyDegree = saltyDegree;
    }
}

/**
 * 肉类
 */
public class Meat extends Material{

}

/**
 * 蔬菜
 */
public class Vegetable extends Material {

}

/**
 * 调料
 */
public class Flavour extends Material {

}


这个实现简单到不能称之为设计,只能说我们通过 Cuisine(菜品) 来表示一个菜品里面必须有的内容,比如配料表

配料表里面的食材我们通过 Material(食材) 类来表示,并根据类型给 Material 创建了三个子类,分别是 Meat(肉)Vegetable(蔬菜)Flavour(调料)。你可能会问,这仨子类有存在的必要吗?这不是仨空类吗?别着急,后面会用到他们

client 是通过 菜单 点菜的,为了让所有的 client 都可以在程序的任意位置都获取到正确的菜单。我们将川菜馆里面所有的菜品都集中到了 Menu(菜单) 中,并只允许 client 通过静态方法获取 Menu 对象


值得注意的是在 Cuisine 中,我只提供了 burdenSheet(配料表) 的 setter,因为将来调用这个模块的未必都是内部的系统,我不可能允许外部系统获取到我的配料表。别人学会了,我喝西北风去?



产地、有机蔬菜和卡路里

开张后第一个问题来了,客户们需要了解自己吃的牛肉是不是从大洋彼岸打飞的来的餐桌、送进嘴里的青椒是不是有机的 以及 咽下去的食物到底含有多少卡路里。也就是说,要求你在电子菜单上提供食材的 生产日期产地热量情况

上帝都发话了,那肯定要开搞,就像这样:

在这里插入图片描述

Cuisine & Material
/**
 * 菜品
 */
public class Cuisine {

    ……

    /**
     * 提供这道菜的热量
     */
    public int getCalorie() {
        //菜品的热量=食材的热量和
        int result = 0;

        for (Material material : burdenSheet) {
            //只有在食材是肉和蔬菜时才计算他的热量
            if (material instanceof Meat) {
                Meat meat = (Meat) material;
                result += meat.getCalorie();
            } else if (material instanceof Vegetable) {
                Vegetable meat = (Vegetable) material;
                result += meat.getCalorie();
            }
        }

        return result;
    }

    /**
     * 是否包含有机蔬菜
     */
    public boolean haveOrganicVegetable(){
        for (Material material : burdenSheet) {
            if(material instanceof Vegetable && ((Vegetable)material).isOrganic()){
                return true;
            }
        }

        return false;
    }
}

/**
 * 肉类
 */
public class Meat extends Material {

    /**
     * 卡路里
     */
    private int calorie;

    /**
     * 产地
     */
    private String productionPlace;

    public int getCalorie() {
        return calorie;
    }

    public void setCalorie(int calorie) {
        this.calorie = calorie;
    }

    public String getProductionPlace() {
        return productionPlace;
    }

    public void setProductionPlace(String productionPlace) {
        this.productionPlace = productionPlace;
    }
}

/**
 * 蔬菜
 */
public class Vegetable extends Material{

    /**
     * 卡路里
     */
    private int calorie;
    /**
     * 是否是有机蔬菜
     */
    private boolean isOrganic;

    public int getCalorie() {
        return calorie;
    }

    public void setCalorie(int calorie) {
        this.calorie = calorie;
    }

    public boolean isOrganic() {
        return isOrganic;
    }

    public void setOrganic(boolean organic) {
        isOrganic = organic;
    }
}


Flavour(调料) 的卡路里是忽略不计的,只有 Meat(肉) 是需要提供产地的,只有 Vegetable(蔬菜) 是区分有机和无机的

如果你将这些带有特殊性的属性全部都写到 Material 根类中,那么随着你对食材的描述越来越完善,这个根类也会复杂到让人害怕,而且有很多属性是没有任何意义的,所以你只能把他们分配到特定的子类中去

但是这种做法带来另一个问题,由于我不能直接公开菜品里的配料表,那就意味着客户的所有定制要求我都需要在 Cuisine 中实现对应的方法。如果只是简单的迭代获取信息倒是也无所谓,但是现在的状况是很多属性依赖的是具体子类的实现,而不是食材的根类,这就让我们必须对实例去做类型判断,才能决定执行什么逻辑


所以虽然上述实现可以完成需求,但是你已经预见到这将是一场噩梦

总有一天会有人希望你添加一个 是否包含香菜 这样的提示;又或者有位穆斯林大哥就要吃鱼香肉丝,你要怎么跟人家解释鱼香肉丝里没有鱼只有猪

至少,我们要找到一种实现可以把这些变化独立出来



访问者

如果你采用访问者改造上面的代码,那么就会得到这样的结果:

在这里插入图片描述

Visitor
/**
 * 访问者
 */
public interface Visitor<E> {

    /**
     * 菜品执行的内容
     */
    E doForCuisine(Cuisine cuisine);

    /**
     * 食材执行的内容
     */
    E doForMaterial(Material material);

    /**
     * 肉类执行的内容
     */
    E doForMeat(Meat meat);

    /**
     * 蔬菜执行的内容
     */
    E doForVegetable(Vegetable vegetable);

    /**
     * 调料执行的内容
     */
    E doForFlavour(Flavour flavour);
}

/**
 * 卡路里访问器
 */
public class CalorieVisitor implements Visitor<Integer>{

    @Override
    public Integer doForCuisine(Cuisine cuisine) {
        int result = 0;
        for (Material material : cuisine.getBurdenSheet()) {
            result += material.accept(this);
        }

        return result;
    }

    @Override
    public Integer doForMaterial(Material material) {
        return 0;
    }

    @Override
    public Integer doForMeat(Meat meat) {
        return meat.getCalorie();
    }

    @Override
    public Integer doForVegetable(Vegetable vegetable) {
        return vegetable.getCalorie();
    }

    @Override
    public Integer doForFlavour(Flavour flavour) {
        return 0;
    }
}

/**
 * 有机属性访问者
 */
public class OrganicVisitor implements Visitor<Boolean> {

    @Override
    public Boolean doForCuisine(Cuisine cuisine) {
        for (Material material : cuisine.getBurdenSheet()) {
            if(material.accept(this)){
                return true;
            }
        }

        return false;
    }

    @Override
    public Boolean doForMaterial(Material material) {
        return false;
    }

    @Override
    public Boolean doForMeat(Meat meat) {
        return false;
    }

    @Override
    public Boolean doForVegetable(Vegetable vegetable) {
        return vegetable.isOrganic();
    }

    @Override
    public Boolean doForFlavour(Flavour flavour) {
        return false;
    }
}
Cuisine & Material
/**
 * 菜品
 */
public class Cuisine {

    //……

    protected List<Material> getBurdenSheet() {
        return burdenSheet;
    }

    public <E> E accept(Visitor<E> v){
        return v.doForCuisine(this);
    }
}

/**
 * 食材
 */
public class Material {

    //……

    public <E> E accept(Visitor<E> v){
        return v.doForMaterial(this);
    }
}

/**
 * 肉类
 */
public class Meat extends Material {

    //……

    public <E> E accept(Visitor<E> v){
        return v.doForMeat(this);
    }
}

/**
 * 蔬菜
 */
public class Vegetable extends Material{

    // ……

    public <E> E accept(Visitor<E> v){
        return v.doForVegetable(this);
    }
}

/**
 * 调料
 */
public class Flavour extends Material{
    public <E> E accept(Visitor<E> v){
        return v.doForFlavour(this);
    }
}

我们创建了一个全新的Visitor(访问者)类簇,让Visitor去和菜品相关的所有类打交道,并获取其中的信息(这就是一开始说的被访问者必须向访问者公开自己的属性),为此我们还特地在Cuisine中添加了一个受保护的getBurdenSheet,以便访问者获取Cuisine内的信息

那访问者要怎么跟被访问者交互呢?还记得观察者模式吗,在观察者模式里我们给观察者和被观察者都做了修改。访问者是一样的,他不能也不应该直接访问被访问者内的信息,而是需要被访问者对他授权,也就是 accept 方法。但是和观察者模式不同的是,所有的被访问者子类都需要针对accept做出自己的特殊操作


这种实现方式堪称惊艳,就像变魔术一样,让所有的类型判断都消失了

其实仔细想想这些类型判断并不是消失了,而是 重写 帮我们代劳了。因为 MeatVegetableFlavour都是Material的子类,所以当我们在这三者中写入accept动作时,其实是在重写他们从Material中继承的方法。也就是说,如果到时候访问者的那个对象是属于下级子类的实例,那他就会优先调用被重写的accept方法

这写法可比if-else优雅多了,而且就算将来真的需要判断有没有香菜,或者有没有猪肉,只需要添加对应的Visitor子类就可以实现


而这正是一个标准的访问者实现




碎碎念

访问者和双分派

笔者读的书少,第一次看到访问者的实现时真的当场拍案叫绝,这种通过子类重写来避开类型判断的写法真的是太妙了

但是这种写法不是访问者的原创,他的行话叫 双分派(double-dispatch)。这是一种很著名的技术,有些编程语言甚至直接支持这种技术,但不包括Java

我们习惯了通过对象/类去点他里面的属性或者方法,就像这样:

a.b(c);

这时候a和b一定是确定的,只有c是动态变化的。这种模式就叫 单分派(single-dispatch)

而双分派实现的效果是可以让a都变得不确定,这是可能的,上例的accept就实现了这种效果

你有没有想过为什么上例的 dofor…accept 中都会出现调用 this,其实这就是在指定执行对象啊。我没有静态的指定谁调用谁,而是在程序执行到那里是才最终确定是谁调用了谁



访问者和代理

从实现上来看,访问者其实是一种变相的代理模式,说得更具体一点是 保护代理

就像上例我们使用访问者的契机其实是为了保护菜品里的配料表,访问者可以减少外部代码和被访问者之间的交互,特别是被访问者的结构错综复杂的时候,可以简化很多工作



写在最后的碎碎念

《庄子·养生主》中讲了一个叫庖丁的人给梁惠王表演杀牛。梁惠王惊讶于庖丁的杀牛技术,于是问他要怎么学才能像他一样。庖丁说:“因为我学习的是道,而不只是技巧。我刚开始杀牛的时候看到什么都是牛,都想用杀牛的方法去操作。三年后,我眼里就没有牛了,连牛在我眼里都不是牛了。因为我不觉得我是在杀牛,而是在解开他的经络,不是因为别人教我要怎么做,而是我的刀划到那里后自然而然应该这样去做,顺着刀势牛就已经被解了。”

这就是庖丁解牛的典故,我们也常用这个程序来形容某人的技术高超

在实战中使用设计模式和庖丁说的是一样的,23种基础设计模式只是”形“而已,他可能是某种情况下的最优解,但绝不是规则。实战中会遇到各种各样的情形,设计模式未必是正确答案,要不然也不会有反模式了

那你会说,用不上那我还学他干嘛?

你要学形而上的东西,你要学模式里的”道“。不是把模型生搬硬套到自己的实现中,而是去思考以前设计这些模式的人为什么要这样做,是什么思路让他做出这样的选择

直到将来的某一天,我相信一定有这样的某一天,道友你在不考虑设计模式的情况下,也会做出和设计模式一样的选择





万分感谢您看完这篇文章,如果您喜欢这篇文章,欢迎点赞、收藏。还可以通过专栏,查看更多与【设计模式】有关的内容

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/562946.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

初学者如何选择ARM开发硬件?

1&#xff0e; 如果你有做硬件和单片机的经验,建议自己做个最小系统板&#xff1a;假如你从没有做过ARM的开发&#xff0c;建议你一开始不要贪大求全&#xff0c;把所有的应用都做好&#xff0c;因为ARM的启动方式和dsp或单片机有所不同&#xff0c;往往会碰到各种问题&#xf…

设计模式-创建型-抽象工厂模式-Abstract Factory

UML类图 工厂接口类 public interface ProductFactory {Phone phoneProduct();//生产手机Router routerProduct();//生产路由器 } 小米工厂实现类 public class XiaomiFactoryImpl implements ProductFactory {Overridepublic Phone phoneProduct() {return new XiaomiPhone…

使用 kubeadm 进行证书管理

使用 kubeadm 进行证书管理 一&#xff1a;使用 kubeadm 进行证书管理 1.检查证书是否过期 kubeadm certs check-expiration 2.手动续订证书 使用 kubeadm certs renew 命令 可以随时手动续订证书&#xff0c;该命令使用存储在/etc/kubernetes/pki中的 CA (or front-proxy-…

从零开始的vscode配置及安装rust教程

配置vscode的rust环境 下载安装vscodemac 环境1. 下载安装rust2. 配置 mac vscode环境3. 创建一个测试项目 windows 环境1. 安装c运行环境2. 安装配置rustup3. 配置windows vscode环境4. 创建一个测试项目 下载安装vscode 1.官网应用程序下载 vscode&#xff1a;https://code.v…

websocket 请求头报错 Provisional headers are shown 的解决方法

今日简单总结 websocket 使用过程中遇到的问题&#xff0c;主要从以下三个方面来分享&#xff1a; 1、前端部分 websocket 代码 2、使用 koa.js 实现后端 websocket 服务搭建 3、和后端 java Netty 库对接时遇到连接失败问题 一、前端部分 websocket 代码 <template>…

Spark和Hadoop的安装

实验内容和要求 1&#xff0e;安装Hadoop和Spark 进入Linux系统&#xff0c;完成Hadoop伪分布式模式的安装。完成Hadoop的安装以后&#xff0c;再安装Spark&#xff08;Local模式&#xff09;。 2&#xff0e;HDFS常用操作 使用hadoop用户名登录进入Linux系统&#xff0c;启动…

MyBatisPlus详解(二)条件构造器Wrapper、自定义SQL、Service接口

文章目录 前言2 核心功能2.1 条件构造器2.1.1 Wrapper2.1.2 QueryWrapper2.1.3 UpdateWrapper2.1.4 LambdaQueryWrapper 2.2 自定义SQL2.2.1 基本用法2.2.2 多表关联 2.3 Service接口2.3.1 IService2.3.1.1 save2.3.1.2 remove2.3.1.3 update2.3.1.4 get2.3.1.5 list2.3.1.6 co…

AlDente Pro for mac最新激活版:电池长续航软件

AlDente Pro是一款专为Mac用户设计的电池管理工具&#xff0c;旨在提供电池安全和健康管理的一站式解决方案。它具备实时监控电池状态的功能&#xff0c;让用户随时了解电池的电量、充电次数、健康状态等信息。 AlDente Pro for mac最新激活版下载 同时&#xff0c;AlDente Pro…

STM32 DA数字模拟转换原理

单片机学习&#xff01; 目录 前言 一、AD与DA 二、AD与DA硬件电路模型 三、运算放大器 四、运放电路 4.1 电压比较器 4.2 反相放大器 4.3 同相放大器 4.4 电压跟随器 五、DA原理 总结 前言 之前文章讲述了STM32中AD模拟数字转换器的内容&#xff0c;文中AD原理中涉及DA原理的内…

用python selenium实现短视频一键推送

https://github.com/coolEphemeroptera/VIVI 效果如下 demo 支持youtube视频搬运

【数据结构】顺序表:与时俱进的结构解析与创新应用

欢迎来到白刘的领域 Miracle_86.-CSDN博客 系列专栏 数据结构与算法 先赞后看&#xff0c;已成习惯 创作不易&#xff0c;多多支持&#xff01; 目录 一、数据结构的概念 二、顺序表&#xff08;Sequence List&#xff09; 2.1 线性表的概念以及结构 2.2 顺序表分类 …

华为开源自研AI框架昇思MindSpore应用案例:数据处理性能优化

如果你对MindSpore感兴趣&#xff0c;可以关注昇思MindSpore社区 数据是整个深度学习中最重要的一环&#xff0c;因为数据的好坏决定了最终结果的上限&#xff0c;模型的好坏只是去无限逼近这个上限&#xff0c;所以高质量的数据输入&#xff0c;会在整个深度神经网络中起到积极…

嵌入式Linux开发实操(十八):Linux音频ALSA开发

应用程序程序员应该使用库API,而不是内核API。alsa库提供了内核API 100%的功能,但增加了可用性方面的主要改进,使应用程序代码更简单、更美观。未来的修复程序或兼容性代码可能会放在库代码中,而不是放在内核驱动程序中。 使用ALSA API和libasound进行简单的声音播放: /*…

用全连接对手写数字识别案例(附解决TensorFlow2.x没有examples问题)

数据集介绍 数据集直接调用可能出现问题&#xff0c;建议从官网直接下载下来&#xff0c;下载存在这四个文件 手写数字识别数据集下载&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1nqhP4yPNcqefKYs91jp9ng?pwdxe1h 提取码&#xff1a;xe1h 55000行训练数据集&a…

CentOS如何使用Docker部署Plik服务并实现公网访问本地设备上传下载文件

文章目录 1. Docker部署Plik2. 本地访问Plik3. Linux安装Cpolar4. 配置Plik公网地址5. 远程访问Plik6. 固定Plik公网地址7. 固定地址访问Plik 本文介绍如何使用Linux docker方式快速安装Plik并且结合Cpolar内网穿透工具实现远程访问&#xff0c;实现随时随地在任意设备上传或者…

Asciinema:一款强大的终端录屏工具

最近看见一个好的终端录屏工具&#xff0c;现在记录一下并进行分享。 终端录屏工具asciinema是一个免费和开源的解决方案&#xff0c;用于记录终端会话并在网上分享。它支持在终端内直接录制&#xff0c;提供播放、复制粘贴和嵌入功能。安装方面&#xff0c;支持多种操作系统&…

Git 原理及使用 (带动图演示)

文章目录 &#x1f308; Ⅰ Git 安装&#x1f319; 01. Linux - centos &#x1f308; Ⅱ Git 工作区、暂存区和版本库&#x1f319; 01. 认识工作区、暂存区和版本库&#x1f319; 02. 使用 Git 管理工作区的文件 &#x1f308; Ⅲ Git 基本操作&#x1f319; 01. 创建本地仓库…

使用代理绕过网站的反爬机制

最近在尝试收集一些网络指标的数据&#xff0c; 所以&#xff0c; 我又开始做爬虫了。 :) 我们在做爬虫的过程中经常会遇到这样的情况&#xff0c;最初爬虫正常运行&#xff0c;正常抓取数据&#xff0c;一切看起来都是那么的美好&#xff0c;然而一杯茶的功夫可能就会出现错误…

YOLOv9改进策略 | Conv篇 | 利用YOLO-MS的MSBlock二次创新RepNCSPELAN4(全网独家创新)

一、本文介绍 本文给大家带来的改进机制是利用YOLO-MS提出的一种针对于实时目标检测的MSBlock模块(其其实不能算是Conv但是其应该是一整个模块)&#xff0c;我们将其用于RepNCSPELAN中组合出一种新的结构&#xff0c;来替换我们网络中的模块可以达到一种轻量化的作用&#xff…

Vue3+TS版本Uniapp:项目前置操作

作者&#xff1a;前端小王hs 阿里云社区博客专家/清华大学出版社签约作者✍/CSDN百万访问博主/B站千粉前端up主 环境&#xff1a;使用vscode进行开发 如果一开始是使用的HbuilderX&#xff0c;请看hbuilderX创建的uniapp项目转移到vscode 为什么选择vscode&#xff1f;有更好…